qubit_command/command_output.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9#[cfg(unix)]
10use std::os::unix::process::ExitStatusExt;
11use std::{
12 process::ExitStatus,
13 str,
14 time::Duration,
15};
16
17/// Captured output and status information from a finished command.
18///
19/// `CommandOutput` stores retained raw stdout and stderr bytes. When the runner
20/// is configured with per-stream capture limits, the retained bytes may be a
21/// prefix of the full output; use [`Self::stdout_truncated`] and
22/// [`Self::stderr_truncated`] to detect that case. By default, [`Self::stdout`]
23/// and [`Self::stderr`] validate retained bytes as UTF-8 and return
24/// [`str::Utf8Error`] for invalid output. If the command was run with
25/// [`CommandRunner::lossy_output`](crate::CommandRunner::lossy_output) enabled,
26/// the runner also stores lossy UTF-8 text where invalid byte sequences are
27/// replaced with the Unicode replacement character.
28///
29/// # Author
30///
31/// Haixing Hu
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CommandOutput {
34 /// Exit status reported by the process.
35 status: ExitStatus,
36 /// Captured standard output bytes.
37 stdout: Vec<u8>,
38 /// Captured standard error bytes.
39 stderr: Vec<u8>,
40 /// Whether stdout was truncated by the configured capture limit.
41 stdout_truncated: bool,
42 /// Whether stderr was truncated by the configured capture limit.
43 stderr_truncated: bool,
44 /// Duration from process spawn to observed termination.
45 elapsed: Duration,
46 /// Lossy UTF-8 stdout generated by the runner when configured.
47 stdout_text: Option<String>,
48 /// Lossy UTF-8 stderr generated by the runner when configured.
49 stderr_text: Option<String>,
50}
51
52impl CommandOutput {
53 /// Creates command output from captured process data.
54 ///
55 /// # Parameters
56 ///
57 /// * `status` - Process exit status.
58 /// * `stdout` - Captured standard output bytes.
59 /// * `stderr` - Captured standard error bytes.
60 /// * `stdout_truncated` - Whether stdout exceeded the capture limit.
61 /// * `stderr_truncated` - Whether stderr exceeded the capture limit.
62 /// * `elapsed` - Observed process duration.
63 /// * `lossy_output` - Whether text accessors should use lossy UTF-8.
64 ///
65 /// # Returns
66 ///
67 /// A command output value containing the supplied data.
68 #[inline]
69 pub(crate) fn new(
70 status: ExitStatus,
71 stdout: Vec<u8>,
72 stderr: Vec<u8>,
73 stdout_truncated: bool,
74 stderr_truncated: bool,
75 elapsed: Duration,
76 lossy_output: bool,
77 ) -> Self {
78 let stdout_text = if lossy_output {
79 Some(String::from_utf8_lossy(&stdout).into_owned())
80 } else {
81 None
82 };
83 let stderr_text = if lossy_output {
84 Some(String::from_utf8_lossy(&stderr).into_owned())
85 } else {
86 None
87 };
88 Self {
89 status,
90 stdout,
91 stderr,
92 stdout_truncated,
93 stderr_truncated,
94 elapsed,
95 stdout_text,
96 stderr_text,
97 }
98 }
99
100 /// Returns the command exit code.
101 ///
102 /// # Returns
103 ///
104 /// `Some(code)` when the platform reports a numeric process exit code, or
105 /// `None` when the process ended in a way that does not map to a numeric
106 /// code.
107 #[inline]
108 pub fn exit_code(&self) -> Option<i32> {
109 self.status.code()
110 }
111
112 /// Returns the full process exit status.
113 ///
114 /// # Returns
115 ///
116 /// Platform-specific process exit status reported by the operating system.
117 #[inline]
118 pub const fn exit_status(&self) -> &ExitStatus {
119 &self.status
120 }
121
122 /// Returns the signal that terminated the process on Unix platforms.
123 ///
124 /// # Returns
125 ///
126 /// `Some(signal)` when the process was terminated by a signal, otherwise
127 /// `None`.
128 #[cfg(unix)]
129 #[inline]
130 pub fn termination_signal(&self) -> Option<i32> {
131 self.status.signal()
132 }
133
134 /// Returns captured standard output as UTF-8 text.
135 ///
136 /// # Returns
137 ///
138 /// `Ok(&str)` when stdout is valid UTF-8. If the command runner used lossy
139 /// output mode, this returns the stored lossy text even when the original
140 /// bytes were not valid UTF-8.
141 ///
142 /// # Errors
143 ///
144 /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8 and the
145 /// command runner did not enable lossy output mode.
146 #[inline]
147 pub fn stdout(&self) -> Result<&str, str::Utf8Error> {
148 match &self.stdout_text {
149 Some(text) => Ok(text.as_str()),
150 None => str::from_utf8(&self.stdout),
151 }
152 }
153
154 /// Returns captured standard error as UTF-8 text.
155 ///
156 /// # Returns
157 ///
158 /// `Ok(&str)` when stderr is valid UTF-8. If the command runner used lossy
159 /// output mode, this returns the stored lossy text even when the original
160 /// bytes were not valid UTF-8.
161 ///
162 /// # Errors
163 ///
164 /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8 and the
165 /// command runner did not enable lossy output mode.
166 #[inline]
167 pub fn stderr(&self) -> Result<&str, str::Utf8Error> {
168 match &self.stderr_text {
169 Some(text) => Ok(text.as_str()),
170 None => str::from_utf8(&self.stderr),
171 }
172 }
173
174 /// Returns the observed command duration.
175 ///
176 /// # Returns
177 ///
178 /// Duration from process spawn to observed termination.
179 #[inline]
180 pub const fn elapsed(&self) -> Duration {
181 self.elapsed
182 }
183
184 /// Returns the captured standard output bytes.
185 ///
186 /// # Returns
187 ///
188 /// A borrowed slice containing stdout exactly as emitted by the process.
189 #[inline]
190 pub fn stdout_bytes(&self) -> &[u8] {
191 &self.stdout
192 }
193
194 /// Returns the captured standard error bytes.
195 ///
196 /// # Returns
197 ///
198 /// A borrowed slice containing stderr exactly as emitted by the process.
199 #[inline]
200 pub fn stderr_bytes(&self) -> &[u8] {
201 &self.stderr
202 }
203
204 /// Returns whether captured stdout was truncated by a configured limit.
205 ///
206 /// # Returns
207 ///
208 /// `true` when stdout emitted more bytes than the runner retained.
209 #[inline]
210 pub const fn stdout_truncated(&self) -> bool {
211 self.stdout_truncated
212 }
213
214 /// Returns whether captured stderr was truncated by a configured limit.
215 ///
216 /// # Returns
217 ///
218 /// `true` when stderr emitted more bytes than the runner retained.
219 #[inline]
220 pub const fn stderr_truncated(&self) -> bool {
221 self.stderr_truncated
222 }
223}