1use crate::manager::{PackageError, PackageManager, RunOutcome, Runner};
8
9pub const AUR_INFO_URL: &str = "https://aur.archlinux.org/rpc/v5/info";
11
12fn in_aur(runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError> {
14 let url = format!("{AUR_INFO_URL}?arg[]={}", percent_encode(pkg));
15 let RunOutcome {
16 status,
17 stdout,
18 stderr,
19 } = runner.run("curl", &["-fsSL", &url])?;
20 if status != 0 {
21 return Err(PackageError::ExitFailure { status, stderr });
22 }
23 let body: serde_json::Value = serde_json::from_str(&stdout)
24 .map_err(|e| PackageError::Io(std::io::Error::other(format!("AUR response: {e}"))))?;
25 Ok(body["results"]
26 .as_array()
27 .is_some_and(|results| results.iter().any(|r| r["Name"] == pkg)))
28}
29
30fn percent_encode(value: &str) -> String {
33 value
34 .bytes()
35 .map(|b| match b {
36 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
37 (b as char).to_string()
38 }
39 _ => format!("%{b:02X}"),
40 })
41 .collect()
42}
43
44pub struct Pacman;
46
47impl Pacman {
48 fn binary(&self) -> &'static str {
50 if which::which("paru").is_ok() {
51 "paru"
52 } else {
53 "pacman"
54 }
55 }
56}
57
58impl PackageManager for Pacman {
59 fn name(&self) -> &'static str {
60 "pacman"
61 }
62
63 fn is_available(&self) -> bool {
64 which::which("pacman").is_ok()
65 }
66
67 fn exists(&self, runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError> {
71 let RunOutcome { status, .. } = runner.run("pacman", &["-Si", pkg])?;
72 if status == 0 {
73 return Ok(true);
74 }
75 in_aur(runner, pkg)
76 }
77
78 fn is_installed(&self, runner: &dyn Runner, pkg: &str) -> Result<bool, PackageError> {
79 let RunOutcome { status, .. } = runner.run("pacman", &["-Q", pkg])?;
80 match status {
81 0 => Ok(true),
82 _ => Ok(false),
83 }
84 }
85
86 fn install(&self, runner: &dyn Runner, packages: &[String]) -> Result<(), PackageError> {
87 let bin = self.binary();
88 let mut args = vec!["-S", "--noconfirm"];
89 args.extend(packages.iter().map(String::as_str));
90 let RunOutcome { status, stderr, .. } = runner.run_as_root(bin, &args)?;
91 if status != 0 {
92 return Err(PackageError::ExitFailure { status, stderr });
93 }
94 Ok(())
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn percent_encode_keeps_unreserved_and_escapes_the_rest() {
104 assert_eq!(percent_encode("hjkl-bin"), "hjkl-bin");
105 assert_eq!(percent_encode("libc++"), "libc%2B%2B");
106 assert_eq!(percent_encode("a b@1"), "a%20b%401");
107 }
108}