use crate::error::Result;
use crate::types::install::{AurHelper, CascadeMode, CommandSpec, InstallOptions, PrivilegeTool};
use super::shell::validate_package_names;
pub const NO_AUR_HELPER_MESSAGE: &str = "No AUR helper (paru/yay) found.";
const NO_AUR_HELPER_STATUS: u8 = 127;
const OPERAND_TERMINATOR: &str = "--";
pub fn build_pacman_install<S: AsRef<str>>(
names: &[S],
options: &InstallOptions,
) -> Result<CommandSpec> {
validate_non_empty(names, "pacman install")?;
validate_package_names(names, "pacman install")?;
let mut args = vec!["-S".to_string()];
if options.needed {
args.push("--needed".to_string());
}
if options.noconfirm {
args.push("--noconfirm".to_string());
}
push_operands(&mut args, names);
Ok(CommandSpec {
program: "pacman".to_string(),
args,
})
}
pub fn build_aur_install<S: AsRef<str>>(
helper: AurHelper,
names: &[S],
options: &InstallOptions,
) -> Result<CommandSpec> {
validate_non_empty(names, "AUR install")?;
validate_package_names(names, "AUR install")?;
let mut args = vec!["-S".to_string()];
if options.aur_only {
args.push("--aur".to_string());
}
if options.needed {
args.push("--needed".to_string());
}
if options.noconfirm {
args.push("--noconfirm".to_string());
}
push_operands(&mut args, names);
Ok(CommandSpec {
program: helper.binary_name().to_string(),
args,
})
}
pub fn build_remove_command<S: AsRef<str>>(
names: &[S],
cascade: CascadeMode,
noconfirm: bool,
) -> Result<CommandSpec> {
validate_non_empty(names, "remove")?;
validate_package_names(names, "remove")?;
let mut args = vec![cascade.flag().to_string()];
if noconfirm {
args.push("--noconfirm".to_string());
}
push_operands(&mut args, names);
Ok(CommandSpec {
program: "pacman".to_string(),
args,
})
}
fn push_operands<S: AsRef<str>>(args: &mut Vec<String>, names: &[S]) {
args.push(OPERAND_TERMINATOR.to_string());
args.extend(names.iter().map(|n| n.as_ref().to_string()));
}
#[must_use]
pub fn build_update_command(helper: Option<AurHelper>, noconfirm: bool) -> CommandSpec {
let program = helper.map_or("pacman", AurHelper::binary_name).to_string();
let mut args = vec!["-Syu".to_string()];
if noconfirm {
args.push("--noconfirm".to_string());
}
CommandSpec { program, args }
}
#[must_use]
pub fn build_force_sync_update_command(helper: Option<AurHelper>, noconfirm: bool) -> CommandSpec {
let program = helper.map_or("pacman", AurHelper::binary_name).to_string();
let mut args = vec!["-Syyu".to_string()];
if noconfirm {
args.push("--noconfirm".to_string());
}
CommandSpec { program, args }
}
#[must_use]
pub fn build_aur_update_command(helper: AurHelper, noconfirm: bool) -> CommandSpec {
let mut args = vec!["-Sua".to_string()];
if noconfirm {
args.push("--noconfirm".to_string());
}
CommandSpec {
program: helper.binary_name().to_string(),
args,
}
}
pub fn aur_install_shell_fallback<S: AsRef<str>>(
names: &[S],
options: &InstallOptions,
) -> Result<String> {
validate_non_empty(names, "AUR install")?;
validate_package_names(names, "AUR install")?;
let mut flags = String::from("-S");
if options.aur_only {
flags.push_str(" --aur");
}
if options.needed {
flags.push_str(" --needed");
}
if options.noconfirm {
flags.push_str(" --noconfirm");
}
let joined = names
.iter()
.map(std::convert::AsRef::as_ref)
.collect::<Vec<_>>()
.join(" ");
Ok(helper_fallback_body(&format!(
"{flags} {OPERAND_TERMINATOR} {joined}"
)))
}
#[must_use]
pub fn aur_update_shell_fallback(noconfirm: bool) -> String {
let flags = if noconfirm {
"-Sua --noconfirm"
} else {
"-Sua"
};
helper_fallback_body(flags)
}
fn helper_fallback_body(tail: &str) -> String {
format!(
"(if command -v paru >/dev/null 2>&1; then paru {tail}; \
elif command -v yay >/dev/null 2>&1; then yay {tail}; \
else echo '{NO_AUR_HELPER_MESSAGE}' >&2; exit {NO_AUR_HELPER_STATUS}; fi)"
)
}
#[must_use]
pub fn with_privilege(tool: PrivilegeTool, spec: CommandSpec) -> CommandSpec {
let mut args = Vec::with_capacity(spec.args.len() + 1);
args.push(spec.program);
args.extend(spec.args);
CommandSpec {
program: tool.binary_name().to_string(),
args,
}
}
fn validate_non_empty<S: AsRef<str>>(names: &[S], context: &str) -> Result<()> {
if names.is_empty() {
return Err(crate::error::ArchToolkitError::EmptyInput {
field: "names".to_string(),
message: format!("at least one package name is required for {context}"),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ArchToolkitError;
#[test]
fn pacman_install_flags() {
let fresh =
build_pacman_install(&["ripgrep"], &InstallOptions::default()).expect("build fresh");
assert_eq!(
fresh.to_shell_string(),
"pacman -S --needed --noconfirm -- ripgrep"
);
let reinstall_opts = InstallOptions {
needed: false,
..Default::default()
};
let reinstall =
build_pacman_install(&["ripgrep"], &reinstall_opts).expect("build reinstall");
assert_eq!(
reinstall.to_shell_string(),
"pacman -S --noconfirm -- ripgrep"
);
let interactive = InstallOptions {
noconfirm: false,
..Default::default()
};
let spec = build_pacman_install(&["a", "b"], &interactive).expect("build interactive");
assert_eq!(spec.to_shell_string(), "pacman -S --needed -- a b");
assert_eq!(spec.args, ["-S", "--needed", "--", "a", "b"]);
}
#[test]
fn aur_install_flags() {
let spec = build_aur_install(AurHelper::Paru, &["yay-bin"], &InstallOptions::default())
.expect("build");
assert_eq!(
spec.to_shell_string(),
"paru -S --aur --needed --noconfirm -- yay-bin"
);
let reinstall = InstallOptions {
needed: false,
..Default::default()
};
let spec2 = build_aur_install(AurHelper::Yay, &["yay-bin"], &reinstall).expect("build");
assert_eq!(
spec2.to_shell_string(),
"yay -S --aur --noconfirm -- yay-bin"
);
let no_aur_flag = InstallOptions {
aur_only: false,
..Default::default()
};
let spec3 = build_aur_install(AurHelper::Paru, &["x"], &no_aur_flag).expect("build");
assert_eq!(spec3.to_shell_string(), "paru -S --needed --noconfirm -- x");
}
#[test]
fn remove_cascade_modes() {
let basic = build_remove_command(&["pkg"], CascadeMode::Basic, false).expect("build basic");
assert_eq!(basic.to_shell_string(), "pacman -R -- pkg");
let cascade =
build_remove_command(&["pkg"], CascadeMode::Cascade, true).expect("build cascade");
assert_eq!(cascade.to_shell_string(), "pacman -Rs --noconfirm -- pkg");
let full = build_remove_command(&["a", "b"], CascadeMode::CascadeWithConfigs, true)
.expect("build full");
assert_eq!(full.to_shell_string(), "pacman -Rns --noconfirm -- a b");
assert_eq!(full.args, ["-Rns", "--noconfirm", "--", "a", "b"]);
}
#[test]
fn update_commands() {
let pacman = build_update_command(None, true);
assert!(!pacman.args.iter().any(|arg| arg == "--"));
assert_eq!(pacman.to_shell_string(), "pacman -Syu --noconfirm");
assert_eq!(
build_update_command(Some(AurHelper::Paru), false).to_shell_string(),
"paru -Syu"
);
}
#[test]
fn force_sync_and_aur_only_updates() {
assert_eq!(
build_force_sync_update_command(None, true).to_shell_string(),
"pacman -Syyu --noconfirm"
);
assert_eq!(
build_force_sync_update_command(Some(AurHelper::Yay), false).to_shell_string(),
"yay -Syyu"
);
assert_eq!(
build_aur_update_command(AurHelper::Paru, true).to_shell_string(),
"paru -Sua --noconfirm"
);
assert_eq!(
build_aur_update_command(AurHelper::Yay, false).to_shell_string(),
"yay -Sua"
);
}
#[test]
fn shell_fallback_bodies() {
let body = aur_install_shell_fallback(&["yay-bin"], &InstallOptions::default())
.expect("build body");
assert_eq!(
body,
"(if command -v paru >/dev/null 2>&1; \
then paru -S --aur --needed --noconfirm -- yay-bin; \
elif command -v yay >/dev/null 2>&1; \
then yay -S --aur --needed --noconfirm -- yay-bin; \
else echo 'No AUR helper (paru/yay) found.' >&2; exit 127; fi)"
);
let update = aur_update_shell_fallback(false);
assert!(update.contains("paru -Sua;"));
assert!(!update.contains("-Sua --"));
assert!(update.contains(NO_AUR_HELPER_MESSAGE));
assert!(update.contains("' >&2; exit 127; fi)"));
let inj = aur_install_shell_fallback(&["bad;rm -rf /"], &InstallOptions::default());
assert!(matches!(
inj,
Err(ArchToolkitError::InvalidPackageName { .. })
));
let empty: [&str; 0] = [];
assert!(matches!(
aur_install_shell_fallback(&empty, &InstallOptions::default()),
Err(ArchToolkitError::EmptyInput { .. })
));
}
#[test]
fn privilege_wrapping() {
let spec = build_pacman_install(&["vim"], &InstallOptions::default()).expect("build");
let sudo = with_privilege(PrivilegeTool::Sudo, spec.clone());
assert_eq!(
sudo.to_shell_string(),
"sudo pacman -S --needed --noconfirm -- vim"
);
assert_eq!(
sudo.args,
["pacman", "-S", "--needed", "--noconfirm", "--", "vim"]
);
let doas = with_privilege(PrivilegeTool::Doas, spec);
assert_eq!(
doas.to_shell_string(),
"doas pacman -S --needed --noconfirm -- vim"
);
}
#[test]
fn validation_errors() {
for evil in ["--help", "-S", ".hidden"] {
assert!(
build_pacman_install(&[evil], &InstallOptions::default()).is_err(),
"pacman install should reject {evil}"
);
assert!(
build_aur_install(AurHelper::Paru, &[evil], &InstallOptions::default()).is_err(),
"AUR install should reject {evil}"
);
assert!(
build_remove_command(&[evil], CascadeMode::Basic, true).is_err(),
"remove should reject {evil}"
);
assert!(
aur_install_shell_fallback(&[evil], &InstallOptions::default()).is_err(),
"shell fallback should reject {evil}"
);
}
let inj = build_pacman_install(&["good", "bad;rm -rf /"], &InstallOptions::default());
assert!(matches!(
inj,
Err(ArchToolkitError::InvalidPackageName { .. })
));
let empty: [&str; 0] = [];
let none = build_pacman_install(&empty, &InstallOptions::default());
assert!(matches!(none, Err(ArchToolkitError::EmptyInput { .. })));
let aur_inj = build_aur_install(AurHelper::Paru, &["$(evil)"], &InstallOptions::default());
assert!(matches!(
aur_inj,
Err(ArchToolkitError::InvalidPackageName { .. })
));
let rm_inj = build_remove_command(&["a b"], CascadeMode::Basic, true);
assert!(matches!(
rm_inj,
Err(ArchToolkitError::InvalidPackageName { .. })
));
}
}