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