use super::PackageManager; use os_info::{Info, Type}; use serde::Deserialize; use serde_yaml::Value; use tokio::process::Command; #[derive(Clone)] pub struct Pacman; #[derive(Deserialize)] struct PacmanRepo { name: String, server: String, cacheserver: Option, siglevel: Option, keyid: Option, keypackage: Option, } struct SigLevel { raw: String, } impl SigLevel { pub fn new(raw: String) -> Self { for token in raw.split_whitespace() { if !VALID_SIGLEVEL_TOKENS.contains(&token) { log::warn!("Invalid siglevel token: {}", token); } } Self { raw } } } impl std::fmt::Display for SigLevel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.raw) } } impl<'de> Deserialize<'de> for SigLevel { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; Ok(SigLevel::new(s)) } } const VALID_SIGLEVEL_TOKENS: &[&str] = &[ // Token "Never", "Optional", "Required", "TrustedOnly", "TrustAll", // Package prefix "PackageNever", "PackageOptional", "PackageRequired", "PackageTrustedOnly", "PackageTrustAll", // Database prefix "DatabaseNever", "DatabaseOptional", "DatabaseRequired", "DatabaseTrustedOnly", "DatabaseTrustAll", ]; impl PackageManager for Pacman { fn name(&self) -> &'static str { "pacman" } fn adopt(&self, distro: &Info) -> bool { matches!( distro.os_type(), Type::Arch | Type::Manjaro | Type::EndeavourOS | Type::Garuda | Type::CachyOS | Type::Artix | Type::Mabox | Type::Nobara | Type::InstantOS ) } fn binary(&self) -> &'static str { "pacman" } fn update(&self) -> Vec { let mut cmd = Command::new("pacman"); cmd.arg("-Sy"); vec![cmd] } fn install(&self, package: &[String]) -> Vec { let mut cmd = Command::new("pacman"); cmd.arg("-S"); cmd.arg("--noconfirm"); cmd.arg("--needed"); cmd.args(package); vec![cmd] } fn change_mirror(&self, repo: &str, _osinfo: Option<&Info>) -> Vec { let mirrorlist_path = "/etc/pacman.d/mirrorlist"; let content = format!( "# Generated by WSExecutor-PM\n\ # Date: $(date)\n\ # Original content overwritten.\n\ \n\ Server = {}\n", repo ); let mut cmd = Command::new("sh"); cmd.arg("-c").arg(format!( "cat > {} << 'PACMAN_EOF'\n{}\nPACMAN_EOF", mirrorlist_path, content )); let mut keyring_cmd = Command::new("sh"); keyring_cmd.arg("-c").arg("pacman-key --init"); vec![cmd, keyring_cmd] } fn add_repository(&self, repo: &Value, _osinfo: Option<&Info>) -> Vec { let repos: Vec = match serde_yaml::from_value(repo.clone()) { Ok(r) => r, Err(e) => { log::error!("Failed to parse pacman repository: {}", e); return Vec::new(); } }; let pacman_conf = "/etc/pacman.conf"; let mut content = String::with_capacity(512); content.push_str("# Modified by HBW baker\n"); let mut cmds = vec![]; for repo in &repos { // Install keyring if repo.keyid.is_some() && repo.keypackage.is_some() { log::error!( "Both keyid and keypackage are set for repository {}", repo.name ); log::warn!("Ignoring..."); continue; } if let Some(keyid) = &repo.keyid { let mut cmd = Command::new("sh"); cmd.arg("-c") .arg(format!("pacman-key --recv-keys {}", keyid)); cmds.push(cmd); let mut cmd = Command::new("sh"); cmd.arg("-c") .arg(format!("pacman-key --lsign-key {}", keyid)); cmds.push(cmd); } else if let Some(keypackage) = &repo.keypackage { let mut cmd = Command::new("sh"); cmd.arg("-c") .arg(format!("pacman -Sy {} --needed --noconfirm", keypackage)); cmds.push(cmd); } // Configure repository let mut block = format!("[{}]\n", &repo.name); block.push_str(&format!("Server = {}\n", &repo.server)); if let Some(cacheserver) = &repo.cacheserver { block.push_str(&format!("CacheServer = {}\n", cacheserver)); } if let Some(siglevel) = &repo.siglevel { block.push_str(&format!("SigLevel = {}\n", siglevel)); } content.push_str(&block); } let mut cmd = Command::new("sh"); cmd.arg("-c").arg(format!( "cat >> {} << 'PACMAN_EOF'\n{}\nPACMAN_EOF", pacman_conf, content )); cmds.insert(0, cmd); cmds } } #[cfg(test)] mod tests { use super::*; #[test] fn test_name() { let pacman = Pacman; assert_eq!(pacman.name(), "pacman"); } #[test] fn test_binary() { let pacman = Pacman; assert_eq!(pacman.binary(), "pacman"); } #[test] fn test_adopt_true_for_arch_based_distros() { let pacman = Pacman; let arch_distros = [ Type::Arch, Type::Manjaro, Type::EndeavourOS, Type::Garuda, Type::CachyOS, Type::Artix, Type::Mabox, Type::Nobara, Type::InstantOS, ]; for distro in arch_distros { let info = Info::with_type(distro); assert!( pacman.adopt(&info), "Pacman should adopt distro: {:?}", distro ); } } #[test] fn test_adopt_false_for_non_arch_distros() { let pacman = Pacman; let non_arch_distros = [ Type::Ubuntu, Type::Debian, Type::Fedora, Type::CentOS, Type::RedHatEnterprise, Type::openSUSE, Type::Gentoo, Type::FreeBSD, Type::Macos, Type::Windows, ]; for distro in non_arch_distros { let info = Info::with_type(distro); assert!( !pacman.adopt(&info), "Pacman should NOT adopt distro: {:?}", distro ); } } #[test] fn test_update_command_contents() { let cmds = Pacman.update(); let cmd = cmds[0].as_std(); assert_eq!(cmd.get_program().to_string_lossy(), "pacman"); let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert_eq!(args, vec!["-Sy"]); } #[test] fn test_install_command_contents() { let cmds = Pacman.install(&["curl".to_string(), "wget".to_string()]); let cmd = cmds[0].as_std(); assert_eq!(cmd.get_program().to_string_lossy(), "pacman"); let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert_eq!(args, vec!["-S", "--noconfirm", "--needed", "curl", "wget"]); } #[test] fn test_install_single_package() { let cmds = Pacman.install(&["package".to_string()]); let cmd = cmds[0].as_std(); let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert_eq!(args, vec!["-S", "--noconfirm", "--needed", "package"]); } #[test] fn test_siglevel_valid_tokens() { let siglevel = SigLevel::new("Never".to_string()); assert_eq!(siglevel.to_string(), "Never"); let siglevel = SigLevel::new("Optional Required".to_string()); assert_eq!(siglevel.to_string(), "Optional Required"); let siglevel = SigLevel::new("PackageOptional DatabaseRequired".to_string()); assert_eq!(siglevel.to_string(), "PackageOptional DatabaseRequired"); } #[test] fn test_siglevel_with_invalid_tokens() { // Invalid tokens are logged but still accepted let siglevel = SigLevel::new("Never InvalidToken".to_string()); assert_eq!(siglevel.to_string(), "Never InvalidToken"); } #[test] fn test_siglevel_deserialize() { let yaml = "Never"; let siglevel: SigLevel = serde_yaml::from_str(yaml).unwrap(); assert_eq!(siglevel.to_string(), "Never"); let yaml = "PackageOptional DatabaseRequired"; let siglevel: SigLevel = serde_yaml::from_str(yaml).unwrap(); assert_eq!(siglevel.to_string(), "PackageOptional DatabaseRequired"); } #[test] fn test_siglevel_all_valid_tokens() { let all_tokens = VALID_SIGLEVEL_TOKENS.join(" "); let siglevel = SigLevel::new(all_tokens.clone()); assert_eq!(siglevel.to_string(), all_tokens); } #[test] fn test_change_mirror_command() { let cmds = Pacman.change_mirror("https://mirror.example.com/archlinux", None); let cmd = cmds[0].as_std(); assert_eq!(cmd.get_program().to_string_lossy(), "sh"); let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); let full_arg = args.join(" "); assert!(full_arg.contains("cat > /etc/pacman.d/mirrorlist")); assert!(full_arg.contains("https://mirror.example.com/archlinux")); } #[test] fn test_add_repository_single_repo() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); // Should have at least one command (the one to append to pacman.conf) assert!(!cmds.is_empty()); let last_cmd = cmds.last().unwrap().as_std(); let args: Vec = last_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); let full_arg = args.join(" "); assert!(full_arg.contains("cat >> /etc/pacman.conf")); assert!(full_arg.contains("[customrepo]")); assert!(full_arg.contains("https://repo.example.com")); } #[test] fn test_add_repository_with_siglevel() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ( serde_yaml::Value::String("siglevel".to_string()), serde_yaml::Value::String("PackageOptional".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); let last_cmd = cmds.last().unwrap().as_std(); let args: Vec = last_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); let full_arg = args.join(" "); assert!(full_arg.contains("SigLevel = PackageOptional")); } #[test] fn test_add_repository_with_keyid() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ( serde_yaml::Value::String("keyid".to_string()), serde_yaml::Value::String("ABCDEF123456".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); // Should have 3 commands: recv-keys, lsign-key, and append to conf assert_eq!(cmds.len(), 3); let recv_cmd = cmds[1].as_std(); let recv_args: Vec = recv_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!(recv_args .join(" ") .contains("pacman-key --recv-keys ABCDEF123456")); let lsign_cmd = cmds[2].as_std(); let lsign_args: Vec = lsign_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!(lsign_args .join(" ") .contains("pacman-key --lsign-key ABCDEF123456")); } #[test] fn test_add_repository_with_keypackage() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ( serde_yaml::Value::String("keypackage".to_string()), serde_yaml::Value::String("gnupg".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); // Should have 2 commands: install keypackage and append to conf assert_eq!(cmds.len(), 2); let install_cmd = cmds[1].as_std(); let install_args: Vec = install_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); assert!(install_args .join(" ") .contains("pacman -Sy gnupg --needed --noconfirm")); } #[test] fn test_add_repository_with_cacheserver() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ( serde_yaml::Value::String("cacheserver".to_string()), serde_yaml::Value::String("https://cache.example.com".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); let last_cmd = cmds.last().unwrap().as_std(); let args: Vec = last_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); let full_arg = args.join(" "); assert!(full_arg.contains("CacheServer = https://cache.example.com")); } #[test] fn test_add_repository_multiple_repos() { let repos_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("repo1".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo1.example.com".to_string()), ), ] .into_iter() .collect(), ), serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("repo2".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo2.example.com".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repos_yaml, None); let last_cmd = cmds.last().unwrap().as_std(); let args: Vec = last_cmd .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect(); let full_arg = args.join(" "); assert!(full_arg.contains("[repo1]")); assert!(full_arg.contains("[repo2]")); } #[test] fn test_add_repository_both_keyid_and_keypackage_logs_error() { let repo_yaml = serde_yaml::from_value(serde_yaml::Value::Sequence(vec![ serde_yaml::Value::Mapping( [ ( serde_yaml::Value::String("name".to_string()), serde_yaml::Value::String("customrepo".to_string()), ), ( serde_yaml::Value::String("server".to_string()), serde_yaml::Value::String("https://repo.example.com".to_string()), ), ( serde_yaml::Value::String("keyid".to_string()), serde_yaml::Value::String("ABCDEF123456".to_string()), ), ( serde_yaml::Value::String("keypackage".to_string()), serde_yaml::Value::String("gnupg".to_string()), ), ] .into_iter() .collect(), ), ])) .unwrap(); let cmds = Pacman.add_repository(&repo_yaml, None); // Should have no key-related commands when both are set (error case) assert!(cmds.is_empty() || cmds.len() == 1); // Only the conf append cmd if any } #[test] fn test_add_repository_invalid_yaml_returns_empty() { let invalid_yaml = serde_yaml::Value::String("invalid: [yaml".to_string()); let cmds = Pacman.add_repository(&invalid_yaml, None); assert!(cmds.is_empty()); } }