Skip to main content

rite_runtime/
executor.rs

1//! Execution-level error / outcome types and shared helpers used by
2//! [`crate::runner::Executor`] when walking a resolved ceremony.
3
4use crate::actions::ArtifactValue;
5use crate::step_info::StepInfo;
6use crate::transcript::compute_file_fingerprint;
7use rite_model::{ActionType, ArtifactId, Material, MaterialKind, MaterialSource, Step, StepId};
8use rite_sdk::BackendError;
9use std::fs;
10use std::io;
11use std::io::Write;
12use std::path::{Path, PathBuf};
13use thiserror::Error;
14
15use crate::output_config::OutputConfig;
16
17/// Errors during ceremony execution.
18#[derive(Debug, Error)]
19pub enum ExecutionError {
20    /// Required parameters are missing values.
21    #[error("Validation failed: {0}")]
22    ValidationFailed(String),
23
24    /// The operator chose to stop the ceremony. Not an error condition: the
25    /// active step (if any) is already recorded in the transcript stream, so
26    /// the terminal message stays ceremony-level rather than naming a step.
27    #[error("Ceremony aborted by the operator")]
28    Aborted,
29
30    /// An I/O error occurred.
31    #[error("IO error: {0}")]
32    Io(#[from] io::Error),
33
34    /// A step handler returned a failure.
35    #[error("Step '{step}' failed: {reason}")]
36    StepFailed {
37        /// Step that failed.
38        step: StepId,
39        /// Failure description.
40        reason: String,
41    },
42
43    /// The action type is not registered.
44    #[error("Unknown action: '{0}'")]
45    UnknownAction(ActionType),
46
47    /// Invalid or missing parameters for a handler.
48    #[error("Invalid params: {0}")]
49    InvalidParams(String),
50
51    /// The host operating system failed to provide entropy for the seed.
52    #[error("Failed to gather machine entropy: {0}")]
53    EntropyError(String),
54
55    /// A material could not be loaded.
56    #[error("Failed to load material '{name}': {reason}")]
57    MaterialLoadFailed {
58        /// Material name.
59        name: String,
60        /// Failure description.
61        reason: String,
62    },
63
64    /// An output artifact could not be written.
65    #[error("Failed to write output '{name}': {reason}")]
66    OutputWriteFailed {
67        /// Output name.
68        name: String,
69        /// Failure description.
70        reason: String,
71    },
72
73    /// Transcript writing failed.
74    #[error("Transcript error: {0}")]
75    TranscriptError(String),
76
77    /// A backend returned an error.
78    #[error("Backend error: {0}")]
79    BackendError(#[from] BackendError),
80}
81
82/// Build a `StepInfo` from a resolved `Step`.
83pub(crate) fn step_info_from(step: &Step) -> StepInfo {
84    StepInfo::new(
85        step.id.clone(),
86        step.role.clone(),
87        step.backend.clone(),
88        step.creates.clone(),
89        step.reads_resolved.clone(),
90    )
91}
92
93/// Load a single material into an `ArtifactValue`.
94pub(crate) fn load_material_artifact(
95    name: &str,
96    material: &Material,
97) -> Result<ArtifactValue, ExecutionError> {
98    match &material.kind {
99        MaterialKind::Physical { identifier, .. } => {
100            let text = identifier
101                .as_deref()
102                .or(material.title.as_deref())
103                .unwrap_or(name)
104                .to_string();
105            Ok(ArtifactValue::Text(text))
106        }
107        MaterialKind::Digital { source } => {
108            let source = source
109                .as_ref()
110                .ok_or_else(|| ExecutionError::MaterialLoadFailed {
111                    name: name.to_string(),
112                    reason: "no source provided for digital material".to_string(),
113                })?;
114            match source {
115                MaterialSource::File { file } => {
116                    let bytes = fs::read(file).map_err(|e| ExecutionError::MaterialLoadFailed {
117                        name: name.to_string(),
118                        reason: e.to_string(),
119                    })?;
120                    Ok(ArtifactValue::Bytes(bytes))
121                }
122                MaterialSource::Identifier { identifier } => {
123                    // Digital material with an inline identifier: treat as text content.
124                    Ok(ArtifactValue::Text(identifier.clone()))
125                }
126            }
127        }
128    }
129}
130
131/// Write `bytes` to a freshly created file at `path`, failing if anything already exists there.
132///
133/// Uses `create_new` so the write cannot follow a pre-existing symlink planted at the
134/// destination (which could otherwise redirect artifact bytes outside the run directory) and
135/// cannot clobber an existing file. On Unix the file is created with `0o600` so artifact
136/// material is not world-readable.
137fn write_new_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
138    let mut options = fs::OpenOptions::new();
139    options.write(true).create_new(true);
140    #[cfg(unix)]
141    {
142        use std::os::unix::fs::OpenOptionsExt;
143        options.mode(0o600);
144    }
145    let mut file = options.open(path)?;
146    file.write_all(bytes)?;
147    file.sync_all()
148}
149
150/// Serialize an artifact, write it to disk, and return `(path, sha256-hex, size-bytes, mime-type)`.
151pub(crate) fn write_artifact_to_disk(
152    artifact_id: &ArtifactId,
153    artifact_value: &ArtifactValue,
154    output_config: &OutputConfig,
155) -> Result<(PathBuf, String, u64, Option<String>), ExecutionError> {
156    let serialized =
157        artifact_value
158            .serialize(None)
159            .map_err(|e| ExecutionError::OutputWriteFailed {
160                name: artifact_id.as_str().to_string(),
161                reason: e,
162            })?;
163
164    let path = output_config
165        .artifact_path(artifact_id.as_str(), serialized.extension)
166        .map_err(|e| ExecutionError::OutputWriteFailed {
167            name: artifact_id.as_str().to_string(),
168            reason: e.to_string(),
169        })?;
170
171    write_new_file(&path, &serialized.bytes).map_err(|e| ExecutionError::OutputWriteFailed {
172        name: artifact_id.as_str().to_string(),
173        reason: e.to_string(),
174    })?;
175
176    let hash = compute_file_fingerprint(&path).map_err(|e| ExecutionError::OutputWriteFailed {
177        name: artifact_id.as_str().to_string(),
178        reason: format!("hash computation failed: {e}"),
179    })?;
180
181    let size = fs::metadata(&path)
182        .map_err(|e| ExecutionError::OutputWriteFailed {
183            name: artifact_id.as_str().to_string(),
184            reason: format!("metadata read failed: {e}"),
185        })?
186        .len();
187
188    Ok((path, hash, size, serialized.mime_type))
189}