use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AurHelper {
Paru,
Yay,
}
impl AurHelper {
#[must_use]
pub const fn binary_name(self) -> &'static str {
match self {
Self::Paru => "paru",
Self::Yay => "yay",
}
}
}
impl std::fmt::Display for AurHelper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.binary_name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PrivilegeTool {
Sudo,
Doas,
}
impl PrivilegeTool {
#[must_use]
pub const fn binary_name(self) -> &'static str {
match self {
Self::Sudo => "sudo",
Self::Doas => "doas",
}
}
}
impl std::fmt::Display for PrivilegeTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.binary_name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CascadeMode {
Basic,
Cascade,
CascadeWithConfigs,
}
impl CascadeMode {
#[must_use]
pub const fn flag(self) -> &'static str {
match self {
Self::Basic => "-R",
Self::Cascade => "-Rs",
Self::CascadeWithConfigs => "-Rns",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Basic => "targets only",
Self::Cascade => "remove dependents",
Self::CascadeWithConfigs => "dependents + configs",
}
}
}
impl std::fmt::Display for CascadeMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.description())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandSpec {
pub program: String,
pub args: Vec<String>,
}
impl CommandSpec {
#[must_use]
pub fn new(
program: impl Into<String>,
args: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
program: program.into(),
args: args.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn to_shell_string(&self) -> String {
let mut out = shell_quote_word(&self.program);
for arg in &self.args {
out.push(' ');
out.push_str(&shell_quote_word(arg));
}
out
}
#[must_use]
pub fn to_command(&self) -> std::process::Command {
let mut cmd = std::process::Command::new(&self.program);
cmd.args(&self.args);
cmd
}
}
impl std::fmt::Display for CommandSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_shell_string())
}
}
fn shell_quote_word(word: &str) -> String {
let safe = !word.is_empty()
&& word.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'@' | b'%' | b'^' | b'_' | b'+' | b'=' | b':' | b',' | b'.' | b'/' | b'-'
)
});
if safe {
return word.to_string();
}
crate::install::shell_single_quote(word)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstallOptions {
pub needed: bool,
pub noconfirm: bool,
pub aur_only: bool,
}
impl Default for InstallOptions {
fn default() -> Self {
Self {
needed: true,
noconfirm: true,
aur_only: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enum_mappings() {
assert_eq!(AurHelper::Paru.binary_name(), "paru");
assert_eq!(AurHelper::Yay.binary_name(), "yay");
assert_eq!(PrivilegeTool::Sudo.binary_name(), "sudo");
assert_eq!(PrivilegeTool::Doas.binary_name(), "doas");
assert_eq!(CascadeMode::Basic.flag(), "-R");
assert_eq!(CascadeMode::Cascade.flag(), "-Rs");
assert_eq!(CascadeMode::CascadeWithConfigs.flag(), "-Rns");
assert_eq!(CascadeMode::Cascade.description(), "remove dependents");
}
#[test]
fn command_spec_shell_string() {
let spec = CommandSpec::new("pacman", ["-S", "--needed", "ripgrep"]);
assert_eq!(spec.to_shell_string(), "pacman -S --needed ripgrep");
let tricky = CommandSpec::new("echo", ["it's", "", "a b"]);
assert_eq!(tricky.to_shell_string(), r#"echo 'it'"'"'s' '' 'a b'"#);
}
#[test]
fn command_spec_to_command() {
let spec = CommandSpec::new("pacman", ["-Qq"]);
let cmd = spec.to_command();
assert_eq!(cmd.get_program(), "pacman");
let args: Vec<_> = cmd.get_args().collect();
assert_eq!(args, ["-Qq"]);
}
#[test]
fn install_options_default() {
let opts = InstallOptions::default();
assert!(opts.needed);
assert!(opts.noconfirm);
assert!(opts.aur_only);
}
#[test]
fn serde_roundtrips() {
let helper: AurHelper = serde_json::from_str(
&serde_json::to_string(&AurHelper::Paru).expect("serialize helper"),
)
.expect("deserialize helper");
assert_eq!(helper, AurHelper::Paru);
let tool: PrivilegeTool = serde_json::from_str(
&serde_json::to_string(&PrivilegeTool::Doas).expect("serialize tool"),
)
.expect("deserialize tool");
assert_eq!(tool, PrivilegeTool::Doas);
let mode: CascadeMode = serde_json::from_str(
&serde_json::to_string(&CascadeMode::Cascade).expect("serialize mode"),
)
.expect("deserialize mode");
assert_eq!(mode, CascadeMode::Cascade);
let spec: CommandSpec = serde_json::from_str(
&serde_json::to_string(&CommandSpec::new("x", ["y"])).expect("serialize spec"),
)
.expect("deserialize spec");
assert_eq!(spec, CommandSpec::new("x", ["y"]));
}
}