use super::StepResult;
use anyhow::{anyhow, Result};
use std::path::Path;
pub async fn execute_foreach_command(
config: crate::config::command::ForeachConfig,
) -> Result<StepResult> {
let result = crate::cook::execution::foreach::execute_foreach(&config).await?;
Ok(StepResult {
success: result.failed_items == 0,
stdout: format!(
"Foreach completed: {} total, {} successful, {} failed",
result.total_items, result.successful_items, result.failed_items
),
stderr: if result.failed_items > 0 {
format!("{} items failed", result.failed_items)
} else {
String::new()
},
exit_code: Some(if result.failed_items == 0 { 0 } else { 1 }),
json_log_location: None,
})
}
pub async fn execute_write_file_command(
config: &crate::config::command::WriteFileConfig,
working_dir: &Path,
) -> Result<StepResult> {
use crate::config::command::WriteFileFormat;
use crate::cook::error::ResultExt;
use std::fs;
if config.path.contains("..") {
return Err(anyhow!(
"Invalid path: parent directory traversal not allowed"
));
}
let file_path = working_dir.join(&config.path);
if config.create_dirs {
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create dirs for {}", file_path.display()))
.map_err(|e| anyhow::Error::msg(e.to_string()))?;
}
}
let content = match config.format {
WriteFileFormat::Text => config.content.clone(),
WriteFileFormat::Json => serde_json::to_string_pretty(
&serde_json::from_str::<serde_json::Value>(&config.content)
.map_err(|e| anyhow!("Invalid JSON: {}", e))?,
)?,
WriteFileFormat::Yaml => serde_yaml::to_string(
&serde_yaml::from_str::<serde_yaml::Value>(&config.content)
.map_err(|e| anyhow!("Invalid YAML: {}", e))?,
)?,
};
fs::write(&file_path, &content)
.with_context(|| format!("Failed to write {}", file_path.display()))
.map_err(|e| anyhow::Error::msg(e.to_string()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode =
u32::from_str_radix(&config.mode, 8).map_err(|e| anyhow!("Invalid mode: {}", e))?;
fs::set_permissions(&file_path, fs::Permissions::from_mode(mode))?;
}
Ok(StepResult {
success: true,
exit_code: Some(0),
stdout: format!("Wrote {} bytes to {}", content.len(), config.path),
stderr: String::new(),
json_log_location: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_execute_write_file_text() {
let temp_dir = TempDir::new().unwrap();
let config = crate::config::command::WriteFileConfig {
path: "test.txt".to_string(),
content: "hello world".to_string(),
format: crate::config::command::WriteFileFormat::Text,
mode: "644".to_string(),
create_dirs: false,
};
let result = execute_write_file_command(&config, temp_dir.path())
.await
.unwrap();
assert!(result.success);
assert!(result.stdout.contains("11 bytes"));
let written = std::fs::read_to_string(temp_dir.path().join("test.txt")).unwrap();
assert_eq!(written, "hello world");
}
#[tokio::test]
async fn test_execute_write_file_json() {
let temp_dir = TempDir::new().unwrap();
let config = crate::config::command::WriteFileConfig {
path: "test.json".to_string(),
content: r#"{"key":"value"}"#.to_string(),
format: crate::config::command::WriteFileFormat::Json,
mode: "644".to_string(),
create_dirs: false,
};
let result = execute_write_file_command(&config, temp_dir.path())
.await
.unwrap();
assert!(result.success);
let written = std::fs::read_to_string(temp_dir.path().join("test.json")).unwrap();
assert!(written.contains("key"));
assert!(written.contains("value"));
}
#[tokio::test]
async fn test_execute_write_file_creates_dirs() {
let temp_dir = TempDir::new().unwrap();
let config = crate::config::command::WriteFileConfig {
path: "nested/dir/test.txt".to_string(),
content: "content".to_string(),
format: crate::config::command::WriteFileFormat::Text,
mode: "644".to_string(),
create_dirs: true,
};
let result = execute_write_file_command(&config, temp_dir.path())
.await
.unwrap();
assert!(result.success);
assert!(temp_dir.path().join("nested/dir/test.txt").exists());
}
#[tokio::test]
async fn test_execute_write_file_rejects_traversal() {
let temp_dir = TempDir::new().unwrap();
let config = crate::config::command::WriteFileConfig {
path: "../escape.txt".to_string(),
content: "malicious".to_string(),
format: crate::config::command::WriteFileFormat::Text,
mode: "644".to_string(),
create_dirs: false,
};
let result = execute_write_file_command(&config, temp_dir.path()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("traversal"));
}
#[tokio::test]
async fn test_execute_write_file_invalid_json() {
let temp_dir = TempDir::new().unwrap();
let config = crate::config::command::WriteFileConfig {
path: "test.json".to_string(),
content: "not valid json".to_string(),
format: crate::config::command::WriteFileFormat::Json,
mode: "644".to_string(),
create_dirs: false,
};
let result = execute_write_file_command(&config, temp_dir.path()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid JSON"));
}
}