Skip to content
Clone
e1df092
187 lines 4.8 KB Rust
use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub enum CmdError {
Spawn {
prog: String,
source: std::io::Error,
},
Io {
path: PathBuf,
source: std::io::Error,
},
NonZeroExit {
prog: String,
args: Vec<String>,
code: i32,
stderr: String,
},
Timeout {
prog: String,
timeout_ms: u64,
},
InvalidArgs(String),
Cancelled,
Pipe(String),
PayloadTooLarge {
cap_bytes: u64,
actual_bytes: u64,
},
Custom(String),
}
impl fmt::Display for CmdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Spawn { prog, source } => {
write!(f, "failed to spawn `{}`: {}", prog, source)
}
Self::Io { path, source } => {
write!(f, "io error at `{}`: {}", path.display(), source)
}
Self::NonZeroExit {
prog,
args,
code,
stderr,
} => {
let argv_summary = redact_argv(prog, args);
write!(
f,
"`{} {}` exited with code {}: {}",
prog,
argv_summary,
code,
stderr.trim()
)
}
Self::Timeout { prog, timeout_ms } => {
write!(f, "`{}` timed out after {}ms", prog, timeout_ms)
}
Self::InvalidArgs(msg) => write!(f, "invalid args: {}", msg),
Self::Cancelled => write!(f, "command was cancelled"),
Self::Pipe(msg) => write!(f, "pipe error: {}", msg),
Self::PayloadTooLarge {
cap_bytes,
actual_bytes,
} => write!(
f,
"output exceeds cap: {} bytes > {} bytes cap",
actual_bytes, cap_bytes
),
Self::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for CmdError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Spawn { source, .. } | Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for CmdError {
fn from(source: std::io::Error) -> Self {
Self::Io {
path: PathBuf::new(),
source,
}
}
}
pub type CmdResult<T> = std::result::Result<T, CmdError>;
const REDACTED: &str = "***REDACTED***";
fn redact_argv(prog: &str, args: &[String]) -> String {
let mut out = String::new();
out.push_str(prog);
let mut redact_next = false;
for arg in args {
out.push(' ');
if redact_next {
out.push_str(REDACTED);
redact_next = false;
continue;
}
// Flags whose secret value is the *next* argv element (e.g.
// `git -c http.extraheader=...`) must redact that following element.
if SEPARATE_VALUE_FLAGS.iter().any(|flag| *flag == arg) {
out.push_str(arg);
redact_next = true;
continue;
}
if SENSITIVE_FLAGS
.iter()
.any(|flag| arg == *flag || arg.starts_with(flag))
{
let label = if arg.contains('=') {
arg.split_once('=').map(|(k, _)| k).unwrap_or(arg)
} else {
arg.as_str()
};
out.push_str(label);
out.push('=');
out.push_str(REDACTED);
continue;
}
if URL_VALUE_FLAGS
.iter()
.any(|flag| arg == *flag || arg.starts_with(flag))
{
let label = if arg.contains('=') {
arg.split_once('=').map(|(k, _)| k).unwrap_or(arg)
} else {
arg.as_str()
};
out.push_str(label);
out.push_str(" <value>");
continue;
}
if looks_like_url(arg) {
out.push_str(REDACTED);
continue;
}
out.push_str(arg);
}
out
}
fn looks_like_url(s: &str) -> bool {
if s.starts_with("http://")
|| s.starts_with("https://")
|| s.starts_with("ssh://")
|| s.starts_with("git://")
|| s.starts_with("file://")
{
true
} else {
s.contains("://") && s.contains('@')
}
}
static SEPARATE_VALUE_FLAGS: &[&str] = &["-c"];
static SENSITIVE_FLAGS: &[&str] = &[
"--upload-pack=",
"--receive-pack=",
"--config=",
"--config-env=",
"-S",
"--gpg-sign=",
"--key-id=",
"--ssh-command=",
"--askpass=",
];
static URL_VALUE_FLAGS: &[&str] = &[
"remote.add",
"remote set-url",
"remote prune",
"--exec=",
"--uploadarchive=",
];