use super::error::{ElenchusError, Result};
use std::{
env,
ffi::OsStr,
fmt::Write as _,
fs::{self, File},
io::{BufReader, Read as _, Write as _},
path::{Path, PathBuf},
process::{Command, ExitStatus, Output, Stdio},
};
pub(super) fn require_on_path(program: &str) -> Result<PathBuf> {
resolve_on_path(program).ok_or_else(|| {
ElenchusError::usage(format!("error: {program} is not installed or not on PATH"))
})
}
pub(super) fn resolve_on_path(program: &str) -> Option<PathBuf> {
let path = Path::new(program);
if path.components().count() > 1 {
return path.is_file().then(|| path.to_path_buf());
}
env::var_os("PATH").and_then(|paths| {
env::split_paths(&paths)
.map(|dir| dir.join(program))
.find(|candidate| candidate.is_file())
})
}
pub(super) fn command_text<I, S>(program: &str, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = command_output(program, args)?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
pub(super) fn checked_command<I, S>(program: &str, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
let status = Command::new(program)
.args(&args)
.status()
.map_err(|error| {
ElenchusError::failure(format!(
"error: failed to run {}: {error}",
command_display(program, &args)
))
})?;
if status.success() {
Ok(())
} else {
Err(ElenchusError::failure(format!(
"error: command failed with status {}: {}",
status_code_text(status),
command_display(program, &args)
)))
}
}
fn command_output<I, S>(program: &str, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
let output = Command::new(program)
.args(&args)
.output()
.map_err(|error| {
ElenchusError::failure(format!(
"error: failed to run {}: {error}",
command_display(program, &args)
))
})?;
if output.status.success() {
Ok(output)
} else {
Err(ElenchusError::failure(format!(
"error: command failed with status {}: {}\n{}",
status_code_text(output.status),
command_display(program, &args),
combined_output_text(&output)
)))
}
}
pub(super) fn command_output_allow_failure<I, S>(program: &str, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
Command::new(program).args(&args).output().map_err(|error| {
ElenchusError::failure(format!(
"error: failed to run {}: {error}",
command_display(program, &args)
))
})
}
pub(super) fn git_text<I, S>(args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
command_text("git", args)
}
pub(super) fn git_status<I, S>(args: I) -> Result<ExitStatus>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
Command::new("git").args(&args).status().map_err(|error| {
ElenchusError::failure(format!(
"error: failed to run {}: {error}",
command_display("git", &args)
))
})
}
pub(super) fn git_status_silent<I, S>(args: I) -> Result<ExitStatus>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
Command::new("git")
.args(&args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|error| {
ElenchusError::failure(format!(
"error: failed to run {}: {error}",
command_display("git", &args)
))
})
}
pub(super) fn git_checked<I, S>(args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
checked_command("git", args)
}
pub(super) fn git_hash_file(path: &Path) -> Result<String> {
command_text("git", ["hash-object", &path.display().to_string()])
}
pub(super) fn git_hash_stdin(contents: &str) -> Result<String> {
let mut child = Command::new("git")
.args(["hash-object", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|error| {
ElenchusError::failure(format!("error: failed to run git hash-object: {error}"))
})?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| ElenchusError::failure("error: failed to open git hash-object stdin"))?;
stdin.write_all(contents.as_bytes()).map_err(|error| {
ElenchusError::failure(format!(
"error: failed to write git hash-object stdin: {error}"
))
})?;
drop(stdin);
let output = child.wait_with_output().map_err(|error| {
ElenchusError::failure(format!(
"error: failed to wait for git hash-object: {error}"
))
})?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
} else {
Err(ElenchusError::failure(format!(
"error: git hash-object failed: {}",
combined_output_text(&output)
)))
}
}
pub(super) fn write_binary(path: &Path, contents: &[u8]) -> Result<()> {
fs::write(path, contents).map_err(|error| {
ElenchusError::failure(format!(
"error: failed to write {}: {error}",
path.display()
))
})
}
pub(super) fn git_diff_binary_to(path: &Path) -> Result<()> {
let file = File::create(path).map_err(|error| {
ElenchusError::failure(format!(
"error: failed to create {}: {error}",
path.display()
))
})?;
let output = Command::new("git")
.args(["diff", "--binary"])
.stdout(Stdio::from(file))
.stderr(Stdio::piped())
.output()
.map_err(|error| {
ElenchusError::failure(format!("error: failed to run git diff --binary: {error}"))
})?;
if output.status.success() {
Ok(())
} else {
Err(ElenchusError::failure(format!(
"error: git diff --binary failed with status {}:\n{}",
status_code_text(output.status),
String::from_utf8_lossy(&output.stderr)
)))
}
}
pub(super) fn review_transcript_file(path: &Path) -> Result<File> {
File::create(path).map_err(|error| {
ElenchusError::failure(format!(
"error: failed to create {}: {error}",
path.display()
))
})
}
pub(super) fn same_file_bytes(left: &Path, right: &Path) -> Result<bool> {
let left = File::open(left).map_err(|error| {
ElenchusError::failure(format!("error: failed to open {}: {error}", left.display()))
})?;
let right = File::open(right).map_err(|error| {
ElenchusError::failure(format!(
"error: failed to open {}: {error}",
right.display()
))
})?;
readers_have_same_bytes(BufReader::new(left), BufReader::new(right))
}
fn readers_have_same_bytes(mut left: BufReader<File>, mut right: BufReader<File>) -> Result<bool> {
let mut left_chunk = [0_u8; 16 * 1024];
let mut right_chunk = [0_u8; 16 * 1024];
loop {
let left_read = left.read(&mut left_chunk).map_err(|error| {
ElenchusError::failure(format!("error: failed to read comparison input: {error}"))
})?;
let right_read = right.read(&mut right_chunk).map_err(|error| {
ElenchusError::failure(format!("error: failed to read comparison input: {error}"))
})?;
if left_read != right_read {
return Ok(false);
}
if left_read == 0 {
return Ok(true);
}
if left_chunk[..left_read] != right_chunk[..right_read] {
return Ok(false);
}
}
}
pub(super) fn is_non_empty_file(path: &Path) -> bool {
fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
}
pub(super) fn meta_contains(path: &Path, needle: &str) -> Result<bool> {
let contents = fs::read_to_string(path).map_err(|error| {
ElenchusError::failure(format!("error: failed to read {}: {error}", path.display()))
})?;
Ok(contents.lines().any(|line| line == needle))
}
pub(super) fn diff_line_count() -> Result<u64> {
let numstat = git_text(["diff", "--numstat"])?;
Ok(numstat
.lines()
.map(|line| {
let mut columns = line.split_whitespace();
let added = columns
.next()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(0);
let deleted = columns
.next()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(0);
added + deleted
})
.sum())
}
pub(super) fn random_hex() -> Result<String> {
let mut file = File::open("/dev/urandom").map_err(|error| {
ElenchusError::failure(format!("error: failed to open /dev/urandom: {error}"))
})?;
let mut bytes = [0_u8; 16];
file.read_exact(&mut bytes).map_err(|error| {
ElenchusError::failure(format!("error: failed to read /dev/urandom: {error}"))
})?;
let mut hex = String::with_capacity(32);
for byte in bytes {
let _result = write!(&mut hex, "{byte:02x}");
}
Ok(hex)
}
pub(super) fn combined_output_bytes(output: &Output) -> Vec<u8> {
let mut bytes = output.stdout.clone();
bytes.extend_from_slice(&output.stderr);
bytes
}
pub(super) fn combined_output_text(output: &Output) -> String {
String::from_utf8_lossy(&combined_output_bytes(output)).into_owned()
}
pub(super) fn status_code_text(status: ExitStatus) -> String {
status
.code()
.map_or_else(|| String::from("signal"), |code| code.to_string())
}
fn command_display(program: &str, args: &[std::ffi::OsString]) -> String {
let mut text = String::from(program);
for arg in args {
text.push(' ');
text.push_str(&arg.to_string_lossy());
}
text
}