use std::process::Command;
use crate::error::UfwError;
pub(crate) struct CommandRunner;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DeleteOutcome {
Deleted,
NotFound,
}
impl CommandRunner {
pub(crate) fn run(program: &str, args: &[&str]) -> Result<(), UfwError> {
let output = Command::new(program).args(args).output()?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Err(UfwError::CommandFailed {
program: program.to_string(),
args: args.iter().map(|a| a.to_string()).collect(),
stderr,
code: output.status.code(),
})
}
}
pub(crate) fn output(program: &str, args: &[&str]) -> Result<String, UfwError> {
let output = Command::new(program).args(args).output()?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout)
} else {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Err(UfwError::CommandFailed {
program: program.to_string(),
args: args.iter().map(|a| a.to_string()).collect(),
stderr,
code: output.status.code(),
})
}
}
pub(crate) fn delete(program: &str, args: &[&str]) -> Result<DeleteOutcome, UfwError> {
let output = Command::new(program).args(args).output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
match classify_delete_output(&stdout, &stderr, output.status.success()) {
Ok(outcome) => Ok(outcome),
Err(_) => Err(UfwError::CommandFailed {
program: program.to_string(),
args: args.iter().map(|a| a.to_string()).collect(),
stderr: stderr.into_owned(),
code: output.status.code(),
}),
}
}
}
pub(crate) fn classify_delete_output(
stdout: &str,
stderr: &str,
success: bool,
) -> Result<DeleteOutcome, UfwError> {
let deleted = stdout.contains("Rule deleted") || stderr.contains("Rule deleted");
let not_found = stdout.contains("Could not delete non-existent rule")
|| stderr.contains("Could not delete non-existent rule");
if deleted {
Ok(DeleteOutcome::Deleted)
} else if not_found {
Ok(DeleteOutcome::NotFound)
} else if success {
Err(UfwError::ParseError(
"delete command succeeded but output did not contain expected message".into(),
))
} else {
Err(UfwError::ParseError(format!(
"delete command failed with unexpected output: {stderr}"
)))
}
}
pub(crate) fn combine_delete_outcomes(
outcomes: impl IntoIterator<Item = Result<DeleteOutcome, UfwError>>,
) -> Result<bool, UfwError> {
let mut any_deleted = false;
let mut first_error: Option<UfwError> = None;
for outcome in outcomes {
match outcome {
Ok(DeleteOutcome::Deleted) => any_deleted = true,
Ok(DeleteOutcome::NotFound) => {}
Err(e) => {
if first_error.is_none() {
first_error = Some(e);
}
}
}
}
if let Some(err) = first_error {
return Err(err);
}
Ok(any_deleted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_stdout_rule_deleted() {
assert_eq!(
classify_delete_output("Rule deleted\n", "", true).expect("parsed"),
DeleteOutcome::Deleted,
);
}
#[test]
fn classify_stdout_rule_deleted_v6() {
assert_eq!(
classify_delete_output("Rule deleted\nRule deleted (v6)\n", "", true).expect("parsed"),
DeleteOutcome::Deleted,
);
}
#[test]
fn classify_stderr_not_found() {
assert_eq!(
classify_delete_output("", "Could not delete non-existent rule\n", false)
.expect("parsed"),
DeleteOutcome::NotFound,
);
}
#[test]
fn classify_stdout_not_found_v6() {
assert_eq!(
classify_delete_output(
"Could not delete non-existent rule\nCould not delete non-existent rule (v6)\n",
"",
false,
)
.expect("parsed"),
DeleteOutcome::NotFound,
);
}
#[test]
fn classify_mixed_deleted_and_not_found() {
assert_eq!(
classify_delete_output(
"Rule deleted\nCould not delete non-existent rule\n",
"",
false,
)
.expect("parsed"),
DeleteOutcome::Deleted,
);
}
#[test]
fn classify_unexpected_failure() {
let result = classify_delete_output("", "some random error\n", false);
assert!(result.is_err());
}
#[test]
fn classify_success_without_known_message() {
let result = classify_delete_output("OK\n", "", true);
assert!(result.is_err());
}
#[test]
fn combine_all_deleted() {
let outcomes = vec![Ok(DeleteOutcome::Deleted)];
assert!(combine_delete_outcomes(outcomes).expect("ok"));
}
#[test]
fn combine_all_not_found() {
let outcomes = vec![Ok(DeleteOutcome::NotFound)];
assert!(!combine_delete_outcomes(outcomes).expect("ok"));
}
#[test]
fn combine_multiple_not_found() {
let outcomes = vec![Ok(DeleteOutcome::NotFound), Ok(DeleteOutcome::NotFound)];
assert!(!combine_delete_outcomes(outcomes).expect("ok"));
}
#[test]
fn combine_not_found_then_deleted() {
let outcomes = vec![Ok(DeleteOutcome::NotFound), Ok(DeleteOutcome::Deleted)];
assert!(combine_delete_outcomes(outcomes).expect("ok"));
}
#[test]
fn combine_deleted_then_not_found() {
let outcomes = vec![Ok(DeleteOutcome::Deleted), Ok(DeleteOutcome::NotFound)];
assert!(combine_delete_outcomes(outcomes).expect("ok"));
}
#[test]
fn combine_error_only() {
let err = UfwError::Io(std::io::Error::other("boom"));
let outcomes: Vec<Result<DeleteOutcome, UfwError>> = vec![Err(err)];
let result = combine_delete_outcomes(outcomes);
assert!(result.is_err());
}
#[test]
fn combine_not_found_and_error() {
let err = UfwError::Io(std::io::Error::other("boom"));
let outcomes = vec![Ok(DeleteOutcome::NotFound), Err(err)];
let result = combine_delete_outcomes(outcomes);
assert!(result.is_err());
}
#[test]
fn combine_deleted_and_error() {
let err = UfwError::Io(std::io::Error::other("boom"));
let outcomes = vec![Ok(DeleteOutcome::Deleted), Err(err)];
let result = combine_delete_outcomes(outcomes);
assert!(result.is_err());
}
#[test]
fn combine_empty_iterator() {
let outcomes: Vec<Result<DeleteOutcome, UfwError>> = vec![];
assert!(!combine_delete_outcomes(outcomes).expect("ok"));
}
}