use crate::actions::ArtifactValue;
use crate::step_info::StepInfo;
use crate::transcript::compute_file_fingerprint;
use rite_model::{ActionType, ArtifactId, Material, MaterialKind, MaterialSource, Step, StepId};
use rite_sdk::BackendError;
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::output_config::OutputConfig;
#[derive(Debug, Error)]
pub enum ExecutionError {
#[error("Validation failed: {0}")]
ValidationFailed(String),
#[error("Step '{0}' was aborted by user")]
StepAborted(StepId),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Step '{step}' failed: {reason}")]
StepFailed {
step: StepId,
reason: String,
},
#[error("Unknown action: '{0}'")]
UnknownAction(ActionType),
#[error("Invalid params: {0}")]
InvalidParams(String),
#[error("Failed to gather machine entropy: {0}")]
EntropyError(String),
#[error("Failed to load material '{name}': {reason}")]
MaterialLoadFailed {
name: String,
reason: String,
},
#[error("Failed to write output '{name}': {reason}")]
OutputWriteFailed {
name: String,
reason: String,
},
#[error("Transcript error: {0}")]
TranscriptError(String),
#[error("Backend error: {0}")]
BackendError(#[from] BackendError),
}
pub(crate) fn step_info_from(step: &Step) -> StepInfo {
StepInfo::new(
step.id.clone(),
step.role.clone(),
step.backend.clone(),
step.creates.clone(),
step.reads_resolved.clone(),
)
}
pub(crate) fn load_material_artifact(
name: &str,
material: &Material,
) -> Result<ArtifactValue, ExecutionError> {
match &material.kind {
MaterialKind::Physical { identifier, .. } => {
let text = identifier
.as_deref()
.or(material.title.as_deref())
.unwrap_or(name)
.to_string();
Ok(ArtifactValue::Text(text))
}
MaterialKind::Digital { source } => {
let source = source
.as_ref()
.ok_or_else(|| ExecutionError::MaterialLoadFailed {
name: name.to_string(),
reason: "no source provided for digital material".to_string(),
})?;
match source {
MaterialSource::File { file } => {
let bytes = fs::read(file).map_err(|e| ExecutionError::MaterialLoadFailed {
name: name.to_string(),
reason: e.to_string(),
})?;
Ok(ArtifactValue::Bytes(bytes))
}
MaterialSource::Identifier { identifier } => {
Ok(ArtifactValue::Text(identifier.clone()))
}
}
}
}
}
fn write_new_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
pub(crate) fn write_artifact_to_disk(
artifact_id: &ArtifactId,
artifact_value: &ArtifactValue,
output_config: &OutputConfig,
) -> Result<(PathBuf, String, u64, Option<String>), ExecutionError> {
let serialized =
artifact_value
.serialize(None)
.map_err(|e| ExecutionError::OutputWriteFailed {
name: artifact_id.as_str().to_string(),
reason: e,
})?;
let path = output_config
.artifact_path(artifact_id.as_str(), serialized.extension)
.map_err(|e| ExecutionError::OutputWriteFailed {
name: artifact_id.as_str().to_string(),
reason: e.to_string(),
})?;
write_new_file(&path, &serialized.bytes).map_err(|e| ExecutionError::OutputWriteFailed {
name: artifact_id.as_str().to_string(),
reason: e.to_string(),
})?;
let hash = compute_file_fingerprint(&path).map_err(|e| ExecutionError::OutputWriteFailed {
name: artifact_id.as_str().to_string(),
reason: format!("hash computation failed: {e}"),
})?;
let size = fs::metadata(&path)
.map_err(|e| ExecutionError::OutputWriteFailed {
name: artifact_id.as_str().to_string(),
reason: format!("metadata read failed: {e}"),
})?
.len();
Ok((path, hash, size, serialized.mime_type))
}