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, 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 = { 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(" "); continue; } let __inl_cond = { let s = arg; 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('@') } }; if __inl_cond { out.push_str(REDACTED); continue; } out.push_str(arg); } out }; 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 for CmdError { fn from(source: std::io::Error) -> Self { Self::Io { path: PathBuf::new(), source, } } } pub type CmdResult = std::result::Result; const REDACTED: &str = "***REDACTED***"; 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=", ];