cargo_codesign/
subprocess.rs1use std::fmt;
2use std::process::Command;
3
4#[derive(Debug)]
5pub struct RunOutput {
6 pub stdout: String,
7 pub stderr: String,
8 pub success: bool,
9 pub code: Option<i32>,
10}
11
12#[derive(Debug, thiserror::Error)]
13pub enum SubprocessError {
14 #[error("failed to execute {command}: {source}")]
15 SpawnFailed {
16 command: String,
17 source: std::io::Error,
18 },
19}
20
21#[derive(Debug, Clone, Copy)]
26pub enum Arg<'a> {
27 Plain(&'a str),
28 Sensitive(&'a str),
29}
30
31impl<'a> Arg<'a> {
32 pub fn sensitive(s: &'a str) -> Self {
33 Arg::Sensitive(s)
34 }
35
36 pub fn as_str(&self) -> &'a str {
37 match self {
38 Arg::Plain(s) | Arg::Sensitive(s) => s,
39 }
40 }
41}
42
43impl<'a> From<&'a str> for Arg<'a> {
44 fn from(s: &'a str) -> Self {
45 Arg::Plain(s)
46 }
47}
48
49impl fmt::Display for Arg<'_> {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Arg::Plain(s) => f.write_str(s),
53 Arg::Sensitive(_) => f.write_str("****"),
54 }
55 }
56}
57
58pub fn run(command: &str, args: &[&str], verbose: bool) -> Result<RunOutput, SubprocessError> {
60 let typed: Vec<Arg<'_>> = args.iter().map(|&s| Arg::Plain(s)).collect();
61 run_args(command, &typed, verbose)
62}
63
64pub fn run_args(
66 command: &str,
67 args: &[Arg<'_>],
68 verbose: bool,
69) -> Result<RunOutput, SubprocessError> {
70 if verbose {
71 let display: Vec<_> = args.iter().map(ToString::to_string).collect();
72 eprintln!(" $ {} {}", command, display.join(" "));
73 }
74
75 let raw: Vec<&str> = args.iter().map(Arg::as_str).collect();
76 let output =
77 Command::new(command)
78 .args(&raw)
79 .output()
80 .map_err(|e| SubprocessError::SpawnFailed {
81 command: command.to_string(),
82 source: e,
83 })?;
84
85 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
86 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
87
88 if verbose && !stdout.is_empty() {
89 eprint!("{stdout}");
90 }
91 if verbose && !stderr.is_empty() {
92 eprint!("{stderr}");
93 }
94
95 Ok(RunOutput {
96 stdout,
97 stderr,
98 success: output.status.success(),
99 code: output.status.code(),
100 })
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn sensitive_arg_display_is_redacted() {
109 let arg = Arg::sensitive("super-secret-password");
110 assert_eq!(arg.to_string(), "****");
111 }
112
113 #[test]
114 fn plain_arg_display_shows_value() {
115 let arg = Arg::Plain("visible-value");
116 assert_eq!(arg.to_string(), "visible-value");
117 }
118
119 #[test]
120 fn sensitive_arg_as_str_exposes_real_value() {
121 let arg = Arg::sensitive("real-password");
122 assert_eq!(arg.as_str(), "real-password");
123 }
124
125 #[test]
126 fn from_str_creates_plain_arg() {
127 let arg: Arg<'_> = "hello".into();
128 assert_eq!(arg.to_string(), "hello");
129 assert_eq!(arg.as_str(), "hello");
130 }
131
132 #[test]
133 fn mixed_args_display_redacts_only_sensitive() {
134 let args: Vec<Arg<'_>> = vec![
135 "create-keychain".into(),
136 "-p".into(),
137 Arg::sensitive("my-secret"),
138 "keychain-name".into(),
139 ];
140 let display: Vec<String> = args.iter().map(ToString::to_string).collect();
141 assert_eq!(display, &["create-keychain", "-p", "****", "keychain-name"]);
142 }
143}