qubit_command/command_output.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9use std::{
10 str,
11 time::Duration,
12};
13
14/// Captured output and status information from a finished command.
15///
16/// `CommandOutput` always stores raw stdout and stderr bytes. By default,
17/// [`Self::stdout`] and [`Self::stderr`] validate those bytes as UTF-8 and
18/// return [`str::Utf8Error`] for invalid output. If the command was run with
19/// [`CommandRunner::lossy_output`](crate::CommandRunner::lossy_output) enabled,
20/// the runner also stores lossy UTF-8 text where invalid byte sequences are
21/// replaced with the Unicode replacement character. This makes the text
22/// accessors return `Ok(&str)` while preserving the original bytes through
23/// [`Self::stdout_bytes`] and [`Self::stderr_bytes`].
24///
25/// # Author
26///
27/// Haixing Hu
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CommandOutput {
30 /// Exit code reported by the process, or `None` when the platform could not
31 /// represent termination as a numeric code.
32 exit_code: Option<i32>,
33 /// Captured standard output bytes.
34 stdout: Vec<u8>,
35 /// Captured standard error bytes.
36 stderr: Vec<u8>,
37 /// Duration from process spawn to observed termination.
38 elapsed: Duration,
39 /// Lossy UTF-8 stdout generated by the runner when configured.
40 stdout_text: Option<String>,
41 /// Lossy UTF-8 stderr generated by the runner when configured.
42 stderr_text: Option<String>,
43}
44
45impl CommandOutput {
46 /// Creates command output from captured process data.
47 ///
48 /// # Parameters
49 ///
50 /// * `exit_code` - Numeric process exit code, if available.
51 /// * `stdout` - Captured standard output bytes.
52 /// * `stderr` - Captured standard error bytes.
53 /// * `elapsed` - Observed process duration.
54 /// * `lossy_output` - Whether text accessors should use lossy UTF-8.
55 ///
56 /// # Returns
57 ///
58 /// A command output value containing the supplied data.
59 #[inline]
60 pub(crate) fn new(
61 exit_code: Option<i32>,
62 stdout: Vec<u8>,
63 stderr: Vec<u8>,
64 elapsed: Duration,
65 lossy_output: bool,
66 ) -> Self {
67 let stdout_text = if lossy_output {
68 Some(String::from_utf8_lossy(&stdout).into_owned())
69 } else {
70 None
71 };
72 let stderr_text = if lossy_output {
73 Some(String::from_utf8_lossy(&stderr).into_owned())
74 } else {
75 None
76 };
77 Self {
78 exit_code,
79 stdout,
80 stderr,
81 elapsed,
82 stdout_text,
83 stderr_text,
84 }
85 }
86
87 /// Returns the command exit code.
88 ///
89 /// # Returns
90 ///
91 /// `Some(code)` when the platform reports a numeric process exit code, or
92 /// `None` when the process ended in a way that does not map to a numeric
93 /// code.
94 #[inline]
95 pub const fn exit_code(&self) -> Option<i32> {
96 self.exit_code
97 }
98
99 /// Returns captured standard output as UTF-8 text.
100 ///
101 /// # Returns
102 ///
103 /// `Ok(&str)` when stdout is valid UTF-8. If the command runner used lossy
104 /// output mode, this returns the stored lossy text even when the original
105 /// bytes were not valid UTF-8.
106 ///
107 /// # Errors
108 ///
109 /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8 and the
110 /// command runner did not enable lossy output mode.
111 #[inline]
112 pub fn stdout(&self) -> Result<&str, str::Utf8Error> {
113 match &self.stdout_text {
114 Some(text) => Ok(text.as_str()),
115 None => str::from_utf8(&self.stdout),
116 }
117 }
118
119 /// Returns captured standard error as UTF-8 text.
120 ///
121 /// # Returns
122 ///
123 /// `Ok(&str)` when stderr is valid UTF-8. If the command runner used lossy
124 /// output mode, this returns the stored lossy text even when the original
125 /// bytes were not valid UTF-8.
126 ///
127 /// # Errors
128 ///
129 /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8 and the
130 /// command runner did not enable lossy output mode.
131 #[inline]
132 pub fn stderr(&self) -> Result<&str, str::Utf8Error> {
133 match &self.stderr_text {
134 Some(text) => Ok(text.as_str()),
135 None => str::from_utf8(&self.stderr),
136 }
137 }
138
139 /// Returns the observed command duration.
140 ///
141 /// # Returns
142 ///
143 /// Duration from process spawn to observed termination.
144 #[inline]
145 pub const fn elapsed(&self) -> Duration {
146 self.elapsed
147 }
148
149 /// Returns the captured standard output bytes.
150 ///
151 /// # Returns
152 ///
153 /// A borrowed slice containing stdout exactly as emitted by the process.
154 #[inline]
155 pub fn stdout_bytes(&self) -> &[u8] {
156 &self.stdout
157 }
158
159 /// Returns the captured standard error bytes.
160 ///
161 /// # Returns
162 ///
163 /// A borrowed slice containing stderr exactly as emitted by the process.
164 #[inline]
165 pub fn stderr_bytes(&self) -> &[u8] {
166 &self.stderr
167 }
168}