pacsea/logic/preflight/command.rs
1//! Command execution abstraction for preflight operations.
2//!
3//! This module provides the [`CommandRunner`] trait and implementations for
4//! executing system commands, enabling testability through dependency injection.
5
6use std::fmt;
7
8/// What: Abstract command execution interface used for spawning helper
9/// binaries such as `pacman`.
10///
11/// Inputs:
12/// - `program`: Executable name to run (for example, `"pacman"`).
13/// - `args`: Slice of positional arguments passed to the executable.
14///
15/// Output:
16/// - `Ok(String)` containing UTF-8 stdout on success.
17/// - `Err(CommandError)` when the invocation fails or stdout is not valid UTF-8.
18///
19/// # Errors
20/// - Returns `Err(CommandError::Io)` when command spawning or execution fails
21/// - Returns `Err(CommandError::Utf8)` when stdout cannot be decoded as UTF-8
22/// - Returns `Err(CommandError::Failed)` when the command exits with a non-zero status
23///
24/// Details:
25/// - Implementations may stub command results to enable deterministic unit
26/// testing.
27/// - Production code relies on [`SystemCommandRunner`].
28pub trait CommandRunner {
29 /// # Errors
30 /// - Returns `Err(CommandError::Io)` when command spawning or execution fails
31 /// - Returns `Err(CommandError::Utf8)` when stdout cannot be decoded as UTF-8
32 /// - Returns `Err(CommandError::Failed)` when the command exits with a non-zero status
33 fn run(&self, program: &str, args: &[&str]) -> Result<String, CommandError>;
34}
35
36/// What: Real command runner backed by `std::process::Command`.
37///
38/// Inputs: Satisfies the [`CommandRunner`] trait without additional parameters.
39///
40/// Output:
41/// - Executes commands on the host system and captures stdout.
42///
43/// # Errors
44/// - Returns `Err(CommandError::Io)` when command spawning or execution fails
45/// - Returns `Err(CommandError::Utf8)` when stdout cannot be decoded as UTF-8
46/// - Returns `Err(CommandError::Failed)` when the command exits with a non-zero status
47///
48/// Details:
49/// - Errors from `std::process::Command::output` are surfaced as
50/// [`CommandError::Io`].
51#[derive(Default)]
52pub struct SystemCommandRunner;
53
54impl CommandRunner for SystemCommandRunner {
55 fn run(&self, program: &str, args: &[&str]) -> Result<String, CommandError> {
56 let output = std::process::Command::new(program).args(args).output()?;
57 if !output.status.success() {
58 return Err(CommandError::Failed {
59 program: program.to_string(),
60 args: args.iter().map(ToString::to_string).collect(),
61 status: output.status,
62 });
63 }
64 Ok(String::from_utf8(output.stdout)?)
65 }
66}
67
68/// What: Error type capturing command spawning, execution, and decoding
69/// failures.
70///
71/// Inputs: Generated internally by helper routines.
72///
73/// Output: Implements `Display`/`Error` for ergonomic propagation.
74///
75/// Details:
76/// - Represents various failure modes when executing system commands.
77/// - Wraps I/O errors, UTF-8 conversion failures, parsing issues, and
78/// non-success exit statuses.
79#[derive(Debug)]
80pub enum CommandError {
81 /// I/O error occurred.
82 Io(std::io::Error),
83 /// UTF-8 decoding error occurred.
84 Utf8(std::string::FromUtf8Error),
85 /// Command execution failed.
86 Failed {
87 /// Program name that failed.
88 program: String,
89 /// Command arguments.
90 args: Vec<String>,
91 /// Exit status of the failed command.
92 status: std::process::ExitStatus,
93 },
94 /// Parse error when processing command output.
95 Parse {
96 /// Program name that produced invalid output.
97 program: String,
98 /// Field name that failed to parse.
99 field: String,
100 },
101}
102
103impl fmt::Display for CommandError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Io(err) => write!(f, "I/O error: {err}"),
107 Self::Utf8(err) => write!(f, "UTF-8 decoding error: {err}"),
108 Self::Failed {
109 program,
110 args,
111 status,
112 } => {
113 write!(f, "{program:?} {args:?} exited with status {status}")
114 }
115 Self::Parse { program, field } => {
116 write!(
117 f,
118 "{program} output did not contain expected field \"{field}\""
119 )
120 }
121 }
122 }
123}
124
125impl std::error::Error for CommandError {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 match self {
128 Self::Io(err) => Some(err),
129 Self::Utf8(err) => Some(err),
130 Self::Failed { .. } | Self::Parse { .. } => None,
131 }
132 }
133}
134
135impl From<std::io::Error> for CommandError {
136 fn from(value: std::io::Error) -> Self {
137 Self::Io(value)
138 }
139}
140
141impl From<std::string::FromUtf8Error> for CommandError {
142 fn from(value: std::string::FromUtf8Error) -> Self {
143 Self::Utf8(value)
144 }
145}