Skip to main content

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}