Skip to main content

krypt_pkg/
pacman.rs

1//! `pacman` / `paru` package manager implementation (Arch Linux).
2//!
3//! Prefers `paru` if available (adds AUR support), falls back to `pacman`.
4//! Both use the same install syntax. The manager name is always `"pacman"` to
5//! match the `DepsGroup.pacman` config field.
6
7use crate::manager::{PackageError, PackageManager, RunOutcome, Runner};
8
9/// The AUR RPC `info` endpoint; see <https://aur.archlinux.org/rpc>.
10pub const AUR_INFO_URL: &str = "https://aur.archlinux.org/rpc/v5/info";
11
12/// Whether the AUR has a package named exactly `pkg`.
13fn 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
30/// Percent-encode everything but RFC 3986 unreserved characters, so a
31/// package name such as `libc++` survives the query string.
32fn 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
44/// Package manager implementation for Arch Linux (pacman / paru).
45pub struct Pacman;
46
47impl Pacman {
48    /// Binary to use for installation: `paru` if available, else `pacman`.
49    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    /// `pacman -Si` against the sync databases, then the AUR's RPC interface
68    /// (through `curl`, which every Arch install has). An AUR package counts
69    /// as found, although installing it still needs `paru`.
70    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}