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