utf8_command/
lib.rs

1//! Provides the [`Utf8Output`] type, a UTF-8-decoded variant of [`std::process::Output`] (as
2//! produced by [`std::process::Command::output`]).
3//!
4//! Construct [`Utf8Output`] from [`Output`] via the [`TryInto`] or [`TryFrom`] traits:
5//!
6//! ```
7//! # use std::process::Command;
8//! # use std::process::ExitStatus;
9//! # use utf8_command::Utf8Output;
10//! let output: Utf8Output = Command::new("echo")
11//!     .arg("puppy")
12//!     .output()
13//!     .unwrap()
14//!     .try_into()
15//!     .unwrap();
16//! assert_eq!(
17//!     output,
18//!     Utf8Output {
19//!         status: ExitStatus::default(),
20//!         stdout: String::from("puppy\n"),
21//!         stderr: String::from(""),
22//!     },
23//! );
24//! ```
25//!
26//! Error messages will include information about the stream that failed to decode, as well as the
27//! output (with invalid UTF-8 bytes replaced with U+FFFD REPLACEMENT CHARACTER):
28//!
29//! ```
30//! # use std::process::ExitStatus;
31//! # use std::process::Output;
32//! # use utf8_command::Utf8Output;
33//! # use utf8_command::Error;
34//! let invalid = Output {
35//!     status: ExitStatus::default(),
36//!     stdout: Vec::from(b"puppy doggy \xc3\x28"), // Invalid 2-byte sequence.
37//!     stderr: Vec::from(b""),
38//! };
39//!
40//! let err: Result<Utf8Output, Error> = invalid.try_into();
41//! assert_eq!(
42//!     err.unwrap_err().to_string(),
43//!     "Stdout contained invalid utf-8 sequence of 1 bytes from index 12: \"puppy doggy �(\""
44//! );
45//! ```
46
47#![deny(missing_docs)]
48
49use std::fmt::Debug;
50use std::fmt::Display;
51use std::process::ExitStatus;
52use std::process::Output;
53use std::string::FromUtf8Error;
54
55mod context;
56use context::FromUtf8ErrorContext;
57
58const ERROR_CONTEXT_BYTES: usize = 1024;
59
60/// A UTF-8-decoded variant of [`std::process::Output`] (as
61/// produced by [`std::process::Command::output`]).
62///
63/// Construct [`Utf8Output`] from [`Output`] via the [`TryInto`] or [`TryFrom`] traits:
64///
65/// ```
66/// # use std::process::Command;
67/// # use std::process::ExitStatus;
68/// # use utf8_command::Utf8Output;
69/// let output: Utf8Output = Command::new("echo")
70///     .arg("puppy")
71///     .output()
72///     .unwrap()
73///     .try_into()
74///     .unwrap();
75/// assert_eq!(
76///     output,
77///     Utf8Output {
78///         status: ExitStatus::default(),
79///         stdout: String::from("puppy\n"),
80///         stderr: String::from(""),
81///     },
82/// );
83/// ```
84///
85/// Error messages will include information about the stream that failed to decode, as well as the
86/// output (with invalid UTF-8 bytes replaced with U+FFFD REPLACEMENT CHARACTER):
87///
88/// ```
89/// # use std::process::ExitStatus;
90/// # use std::process::Output;
91/// # use utf8_command::Utf8Output;
92/// # use utf8_command::Error;
93/// let invalid = Output {
94///     status: ExitStatus::default(),
95///     stdout: Vec::from(b"\xc3\x28"), // Invalid 2-byte sequence.
96///     stderr: Vec::from(b""),
97/// };
98///
99/// let err: Result<Utf8Output, Error> = invalid.try_into();
100/// assert_eq!(
101///     err.unwrap_err().to_string(),
102///     "Stdout contained invalid utf-8 sequence of 1 bytes from index 0: \"�(\""
103/// );
104/// ```
105///
106/// If there's a lot of output (currently, more than 1024 bytes), only the portion around the
107/// decode error will be shown:
108///
109/// ```
110/// # use std::process::ExitStatus;
111/// # use std::process::Output;
112/// # use utf8_command::Utf8Output;
113/// # use utf8_command::Error;
114/// let mut stdout = vec![];
115/// for _ in 0..300 {
116///     stdout.extend(b"puppy ");
117/// }
118/// // Add an invalid byte:
119/// stdout[690] = 0xc0;
120///
121/// let invalid = Output {
122///     status: ExitStatus::default(),
123///     stdout,
124///     stderr: Vec::from(b""),
125/// };
126///
127/// let err: Result<Utf8Output, Error> = invalid.try_into();
128/// assert_eq!(
129///     err.unwrap_err().to_string(),
130///     "Stdout contained invalid utf-8 sequence of 1 bytes from index 690: \
131///     [178 bytes] \"y puppy puppy puppy puppy puppy puppy puppy puppy puppy \
132///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
133///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
134///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
135///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
136///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
137///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
138///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy �uppy \
139///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
140///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
141///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
142///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
143///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
144///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
145///     puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy puppy \
146///     puppy puppy puppy puppy puppy puppy puppy pu\" [598 bytes]"
147/// );
148/// ```
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Utf8Output {
151    /// The [`std::process::Command`]'s exit status.
152    pub status: ExitStatus,
153    /// The contents of the [`std::process::Command`]'s [`stdout` stream][stdout], decoded as
154    /// UTF-8.
155    ///
156    /// [stdout]: https://linux.die.net/man/3/stdout
157    pub stdout: String,
158    /// The contents of the [`std::process::Command`]'s [`stderr` stream][stdout], decoded as
159    /// UTF-8.
160    ///
161    /// [stdout]: https://linux.die.net/man/3/stdout
162    pub stderr: String,
163}
164
165impl TryFrom<Output> for Utf8Output {
166    type Error = Error;
167
168    fn try_from(
169        Output {
170            status,
171            stdout,
172            stderr,
173        }: Output,
174    ) -> Result<Self, Self::Error> {
175        let stdout =
176            String::from_utf8(stdout).map_err(|err| Error::Stdout(StdoutError { inner: err }))?;
177        let stderr =
178            String::from_utf8(stderr).map_err(|err| Error::Stderr(StderrError { inner: err }))?;
179
180        Ok(Utf8Output {
181            status,
182            stdout,
183            stderr,
184        })
185    }
186}
187
188impl TryFrom<&Output> for Utf8Output {
189    type Error = Error;
190
191    fn try_from(
192        Output {
193            status,
194            stdout,
195            stderr,
196        }: &Output,
197    ) -> Result<Self, Self::Error> {
198        let stdout = String::from_utf8(stdout.to_vec())
199            .map_err(|err| Error::Stdout(StdoutError { inner: err }))?;
200        let stderr = String::from_utf8(stderr.to_vec())
201            .map_err(|err| Error::Stderr(StderrError { inner: err }))?;
202        let status = *status;
203
204        Ok(Utf8Output {
205            status,
206            stdout,
207            stderr,
208        })
209    }
210}
211
212/// An error produced when converting [`Output`] to [`Utf8Output`], wrapping a [`FromUtf8Error`].
213///
214/// ```
215/// use std::process::ExitStatus;
216/// use std::process::Output;
217/// use utf8_command::Utf8Output;
218/// use utf8_command::Error;
219///
220/// let invalid = Output {
221///     status: ExitStatus::default(),
222///     stdout: Vec::from(b""),
223///     stderr: Vec::from(b"\xe2\x28\xa1"), // Invalid 3-byte sequence.
224/// };
225///
226/// let result: Result<Utf8Output, Error> = invalid.try_into();
227/// assert_eq!(
228///     result.unwrap_err().to_string(),
229///     "Stderr contained invalid utf-8 sequence of 1 bytes from index 0: \"�(�\""
230/// );
231/// ```
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum Error {
234    /// The [`Output`]'s stdout field contained invalid UTF-8.
235    Stdout(StdoutError),
236    /// The [`Output`]'s stderr field contained invalid UTF-8.
237    Stderr(StderrError),
238}
239
240impl Error {
241    /// Get a reference to the inner [`FromUtf8Error`].
242    pub fn inner(&self) -> &FromUtf8Error {
243        match self {
244            Error::Stdout(err) => err.inner(),
245            Error::Stderr(err) => err.inner(),
246        }
247    }
248}
249
250impl From<StdoutError> for Error {
251    fn from(value: StdoutError) -> Self {
252        Self::Stdout(value)
253    }
254}
255
256impl From<StderrError> for Error {
257    fn from(value: StderrError) -> Self {
258        Self::Stderr(value)
259    }
260}
261
262impl From<Error> for FromUtf8Error {
263    fn from(value: Error) -> Self {
264        match value {
265            Error::Stdout(err) => err.inner,
266            Error::Stderr(err) => err.inner,
267        }
268    }
269}
270
271impl Display for Error {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match &self {
274            Error::Stdout(err) => write!(f, "{}", err),
275            Error::Stderr(err) => write!(f, "{}", err),
276        }
277    }
278}
279
280impl std::error::Error for Error {}
281
282/// The [`Output`]'s `stdout` field contained invalid UTF-8. Wraps a [`FromUtf8Error`].
283///
284/// ```
285/// use utf8_command::StdoutError;
286///
287/// let invalid_utf8 = Vec::from(b"\x80"); // Invalid single byte.
288/// let inner_err = String::from_utf8(invalid_utf8).unwrap_err();
289/// let err = StdoutError::from(inner_err);
290/// assert_eq!(
291///     err.to_string(),
292///     "Stdout contained invalid utf-8 sequence of 1 bytes from index 0: \"�\""
293/// );
294/// ```
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct StdoutError {
297    inner: FromUtf8Error,
298}
299
300impl StdoutError {
301    /// Get a reference to the inner [`FromUtf8Error`].
302    pub fn inner(&self) -> &FromUtf8Error {
303        &self.inner
304    }
305}
306
307impl From<StdoutError> for FromUtf8Error {
308    fn from(value: StdoutError) -> Self {
309        value.inner
310    }
311}
312
313impl From<FromUtf8Error> for StdoutError {
314    fn from(inner: FromUtf8Error) -> Self {
315        Self { inner }
316    }
317}
318
319impl Display for StdoutError {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        write!(
322            f,
323            "Stdout contained {}: {}",
324            self.inner,
325            FromUtf8ErrorContext::new(&self.inner, ERROR_CONTEXT_BYTES)
326        )
327    }
328}
329
330impl std::error::Error for StdoutError {}
331
332/// The [`Output`]'s `stderr` field contained invalid UTF-8. Wraps a [`FromUtf8Error`].
333///
334/// ```
335/// use utf8_command::StderrError;
336///
337/// let invalid_utf8 = Vec::from(b"\xf0\x90"); // Incomplete 4-byte sequence.
338/// let inner_err = String::from_utf8(invalid_utf8).unwrap_err();
339/// let err = StderrError::from(inner_err);
340/// assert_eq!(
341///     err.to_string(),
342///     "Stderr contained incomplete utf-8 byte sequence from index 0: \"�\""
343/// );
344/// ```
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct StderrError {
347    inner: FromUtf8Error,
348}
349
350impl StderrError {
351    /// Get a reference to the inner [`FromUtf8Error`].
352    pub fn inner(&self) -> &FromUtf8Error {
353        &self.inner
354    }
355}
356
357impl From<StderrError> for FromUtf8Error {
358    fn from(value: StderrError) -> Self {
359        value.inner
360    }
361}
362
363impl From<FromUtf8Error> for StderrError {
364    fn from(inner: FromUtf8Error) -> Self {
365        Self { inner }
366    }
367}
368
369impl Display for StderrError {
370    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371        write!(
372            f,
373            "Stderr contained {}: {}",
374            self.inner,
375            FromUtf8ErrorContext::new(&self.inner, ERROR_CONTEXT_BYTES)
376        )
377    }
378}
379
380impl std::error::Error for StderrError {}