1use std::process::Output;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use util::command::Command;
6
7use crate::devcontainer_api::DevContainerError;
8
9pub(crate) struct DefaultCommandRunner;
10
11impl DefaultCommandRunner {
12 pub(crate) fn new() -> Self {
13 Self
14 }
15}
16
17#[async_trait]
18impl CommandRunner for DefaultCommandRunner {
19 async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error> {
20 command.output().await
21 }
22}
23
24#[async_trait]
25pub(crate) trait CommandRunner: Send + Sync {
26 async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error>;
27}
28
29pub(crate) async fn evaluate_json_command<T>(
30 mut command: Command,
31) -> Result<Option<T>, DevContainerError>
32where
33 T: for<'de> Deserialize<'de>,
34{
35 let output = command.output().await.map_err(|e| {
36 log::error!("Error running command {:?}: {e}", command);
37 DevContainerError::CommandFailed(command.get_program().display().to_string())
38 })?;
39
40 deserialize_json_output(output).map_err(|e| {
41 log::error!("Error running command {:?}: {e}", command);
42 DevContainerError::CommandFailed(command.get_program().display().to_string())
43 })
44}
45
46pub(crate) fn deserialize_json_output<T>(output: Output) -> Result<Option<T>, String>
47where
48 T: for<'de> Deserialize<'de>,
49{
50 if output.status.success() {
51 let raw = String::from_utf8_lossy(&output.stdout);
52 if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
53 return Ok(None);
54 }
55 serde_json_lenient::from_str(&raw)
56 .map_err(|e| format!("Error deserializing from raw json: {e}"))
57 } else {
58 let std_err = String::from_utf8_lossy(&output.stderr);
59 Err(format!(
60 "Sent non-successful output; cannot deserialize. StdErr: {std_err}"
61 ))
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use std::process::ExitStatus;
68
69 use super::*;
70
71 fn success_output(stdout: &str) -> Output {
72 Output {
73 status: ExitStatus::default(),
74 stdout: stdout.as_bytes().to_vec(),
75 stderr: Vec::new(),
76 }
77 }
78
79 #[derive(Debug, Deserialize, PartialEq)]
80 struct TestItem {
81 id: String,
82 }
83
84 #[test]
85 fn test_deserialize_newline_delimited_json_rejected() {
86 // Strict single-value contract: NDJSON must be rejected. Commands that
87 // may legitimately return multiple rows (e.g. `docker ps`) parse their
88 // output themselves rather than routing through this helper.
89 let output = success_output("{\"id\":\"first\"}\n{\"id\":\"second\"}\n");
90 let result: Result<Option<TestItem>, String> = deserialize_json_output(output);
91 assert!(result.is_err(), "expected parse error, got {result:?}");
92 }
93
94 #[test]
95 fn test_deserialize_empty_output() {
96 let output = success_output("");
97 let result: Option<TestItem> = deserialize_json_output(output).unwrap();
98 assert_eq!(result, None);
99 }
100
101 #[test]
102 fn test_deserialize_empty_object() {
103 let output = success_output("{}");
104 let result: Option<TestItem> = deserialize_json_output(output).unwrap();
105 assert_eq!(result, None);
106 }
107}