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 {}