use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use serde_json::Value;
pub async fn materialize_outputs(response: Value) -> Result<Value, Box<dyn std::error::Error>> {
let mut response = response;
let outputs_val = match response.get_mut("outputs") {
Some(v) => v,
None => return Ok(response),
};
let outputs_map = match outputs_val.as_object_mut() {
Some(m) => m,
None => return Ok(response),
};
if outputs_map.is_empty() {
return Ok(response);
}
for (path, entry) in outputs_map.iter_mut() {
let entry_obj = match entry.as_object_mut() {
Some(o) => o,
None => continue,
};
let b64 = match entry_obj.get("content_base64").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => continue,
};
let bytes = B64
.decode(b64.as_bytes())
.map_err(|e| format!("output '{path}' has invalid base64: {e}"))?;
if let Some(parent) = std::path::Path::new(path).parent() {
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("failed to create parent directory for '{path}': {e}"))?;
}
}
tokio::fs::write(path, &bytes)
.await
.map_err(|e| format!("failed to write output '{path}': {e}"))?;
entry_obj.remove("content_base64");
entry_obj.insert("path".to_string(), Value::String(path.clone()));
}
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn no_outputs_field_passes_through() {
let v = json!({"stdout": "ok"});
let out = materialize_outputs(v.clone()).await.unwrap();
assert_eq!(out, v);
}
#[tokio::test]
async fn writes_outputs_to_disk_and_strips_base64() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("shot.png");
let path_str = path.to_string_lossy().to_string();
let bytes = b"fake-png-bytes".to_vec();
let response = json!({
"stdout": "Saved screenshot",
"outputs": {
path_str.clone(): {
"content_base64": B64.encode(&bytes),
"size_bytes": bytes.len(),
"content_type": "image/png",
}
}
});
let materialized = materialize_outputs(response).await.unwrap();
let on_disk = std::fs::read(&path).unwrap();
assert_eq!(on_disk, bytes);
let entry = &materialized["outputs"][&path_str];
assert!(entry.get("content_base64").is_none());
assert_eq!(entry["path"], path_str);
assert_eq!(entry["size_bytes"], bytes.len());
assert_eq!(entry["content_type"], "image/png");
}
#[tokio::test]
async fn invalid_base64_returns_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.bin").to_string_lossy().to_string();
let response = json!({
"outputs": {
path: {"content_base64": "!!!not base64!!!", "size_bytes": 0}
}
});
assert!(materialize_outputs(response).await.is_err());
}
#[tokio::test]
async fn writes_to_deep_path_creates_intermediate_directories() {
let dir = tempfile::tempdir().unwrap();
let deep_path = dir.path().join("a").join("b").join("c").join("frame.png");
let path_str = deep_path.to_string_lossy().to_string();
let bytes = b"deep-bytes".to_vec();
let response = json!({
"outputs": {
path_str.clone(): {
"content_base64": B64.encode(&bytes),
"size_bytes": bytes.len(),
}
}
});
assert!(
!deep_path.parent().unwrap().exists(),
"parent dir must not exist before the call"
);
materialize_outputs(response).await.unwrap();
assert!(deep_path.exists(), "deep path should have been created");
assert_eq!(std::fs::read(&deep_path).unwrap(), bytes);
}
}