use std::fmt;
use std::path::Path;
use std::time::Duration;
use regex::Regex;
use serde::de::DeserializeOwned;
use super::error::{exit_code_info, ERROR_DETAILS_LIMIT};
use super::error::{format_error_details, format_error_message, format_exit_message};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ErrorInfo {
pub message: String,
pub errno: Option<i64>,
pub code: Option<String>,
}
impl ErrorInfo {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
errno: None,
code: None,
}
}
pub fn from_io(err: &std::io::Error) -> Self {
let raw = err.raw_os_error();
let code = match err.kind() {
std::io::ErrorKind::NotFound => Some("ENOENT"),
std::io::ErrorKind::PermissionDenied => Some("EACCES"),
std::io::ErrorKind::AlreadyExists => Some("EEXIST"),
std::io::ErrorKind::BrokenPipe => Some("EPIPE"),
std::io::ErrorKind::InvalidInput => Some("EINVAL"),
_ => None,
};
Self {
message: err.to_string(),
errno: raw.map(|e| -i64::from(e)),
code: code.map(str::to_string),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProcessOutput {
pub stdout: String,
pub stderr: String,
pub stdall: String,
pub exit_code: Option<i32>,
pub signal: Option<String>,
pub duration: Duration,
pub error: Option<ErrorInfo>,
pub from: String,
}
impl ProcessOutput {
pub fn new(
exit_code: Option<i32>,
signal: Option<&str>,
stdout: impl Into<String>,
stderr: impl Into<String>,
stdall: impl Into<String>,
) -> Self {
Self {
stdout: stdout.into(),
stderr: stderr.into(),
stdall: stdall.into(),
exit_code,
signal: signal.map(str::to_string),
..Default::default()
}
}
pub fn from_error(error: ErrorInfo, from: impl Into<String>) -> Self {
Self {
error: Some(error),
from: from.into(),
..Default::default()
}
}
pub fn with_from(mut self, from: impl Into<String>) -> Self {
self.from = from.into();
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn ok(&self) -> bool {
self.error.is_none() && self.exit_code == Some(0)
}
pub fn text(&self) -> String {
self.stdall.clone()
}
pub fn text_hex(&self) -> String {
self.stdall.bytes().map(|b| format!("{b:02x}")).collect()
}
pub fn value_of(&self) -> &str {
self.stdall.trim()
}
pub fn buffer(&self) -> Vec<u8> {
self.stdall.as_bytes().to_vec()
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_str(&self.stdall)
}
pub fn lines(&self) -> Vec<String> {
static NEWLINE: once_cell::sync::Lazy<Regex> =
once_cell::sync::Lazy::new(|| Regex::new(r"\r?\n").expect("valid regex"));
finish_lines(NEWLINE.split(&self.stdall).map(str::to_string).collect())
}
pub fn lines_with(&self, delimiter: &str) -> Vec<String> {
finish_lines(self.stdall.split(delimiter).map(str::to_string).collect())
}
pub fn exit_code_info(&self) -> Option<&'static str> {
self.exit_code.and_then(exit_code_info)
}
pub fn message(&self) -> String {
if let Some(err) = &self.error {
return format_error_message(&err.message, err.errno, err.code.as_deref(), &self.from);
}
let details = if self.stderr.trim().is_empty() {
format_error_details(&self.lines(), ERROR_DETAILS_LIMIT)
} else {
String::new()
};
format_exit_message(
self.exit_code,
self.signal.as_deref(),
&self.stderr,
&self.from,
&details,
)
}
}
fn finish_lines(mut pieces: Vec<String>) -> Vec<String> {
if pieces.last().is_some_and(|l| l.is_empty()) {
pieces.pop();
}
pieces
}
impl fmt::Display for ProcessOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.stdall)
}
}
impl std::error::Error for ProcessOutput {}
impl AsRef<Path> for ProcessOutput {
fn as_ref(&self) -> &Path {
Path::new(self.stdall.trim())
}
}
impl IntoIterator for &ProcessOutput {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.lines().into_iter()
}
}