Skip to main content

rsigma_runtime/sources/
command.rs

1//! Command source resolver: runs a local command and captures stdout.
2
3use std::time::Instant;
4
5use rsigma_eval::pipeline::sources::{DataFormat, ExtractExpr};
6
7use super::extract::apply_extract;
8use super::file::parse_data;
9use super::{ResolvedValue, SourceError, SourceErrorKind};
10
11/// Resolve a command source by executing it and parsing stdout.
12pub async fn resolve_command(
13    command: &[String],
14    format: DataFormat,
15    extract_expr: Option<&ExtractExpr>,
16) -> Result<ResolvedValue, SourceError> {
17    if command.is_empty() {
18        return Err(SourceError {
19            source_id: String::new(),
20            kind: SourceErrorKind::Fetch("command is empty".into()),
21        });
22    }
23
24    let output = tokio::process::Command::new(&command[0])
25        .args(&command[1..])
26        .stdout(std::process::Stdio::piped())
27        .stderr(std::process::Stdio::piped())
28        .spawn()
29        .map_err(|e| SourceError {
30            source_id: String::new(),
31            kind: SourceErrorKind::Fetch(format!("failed to spawn '{}': {e}", command[0])),
32        })?
33        .wait_with_output()
34        .await
35        .map_err(|e| SourceError {
36            source_id: String::new(),
37            kind: SourceErrorKind::Fetch(format!("command execution failed: {e}")),
38        })?;
39
40    if !output.status.success() {
41        let stderr = String::from_utf8_lossy(&output.stderr);
42        return Err(SourceError {
43            source_id: String::new(),
44            kind: SourceErrorKind::Fetch(format!(
45                "command exited with {}: {}",
46                output.status,
47                stderr.trim()
48            )),
49        });
50    }
51
52    let stdout = String::from_utf8(output.stdout).map_err(|e| SourceError {
53        source_id: String::new(),
54        kind: SourceErrorKind::Parse(format!("command output is not valid UTF-8: {e}")),
55    })?;
56
57    let parsed = parse_data(&stdout, format)?;
58
59    let data = if let Some(expr) = extract_expr {
60        apply_extract(&parsed, expr)?
61    } else {
62        parsed
63    };
64
65    Ok(ResolvedValue {
66        data,
67        resolved_at: Instant::now(),
68        from_cache: false,
69    })
70}