pacsea/logic/sandbox/
analyze.rs

1//! Analysis functions for comparing dependencies against host environment.
2
3use crate::logic::sandbox::parse::parse_pkgbuild_deps;
4use crate::logic::sandbox::parse::parse_srcinfo_deps;
5use crate::logic::sandbox::types::{DependencyDelta, SandboxInfo};
6use std::collections::HashSet;
7use std::process::{Command, Stdio};
8
9/// What: Analyze package dependencies from .SRCINFO content.
10///
11/// Inputs:
12/// - `package_name`: AUR package name.
13/// - `srcinfo_text`: .SRCINFO content.
14/// - `installed`: Set of installed package names.
15/// - `provided`: Set of package names provided by installed packages.
16///
17/// Output:
18/// - `SandboxInfo` with dependency deltas.
19pub(super) fn analyze_package_from_srcinfo(
20    package_name: &str,
21    srcinfo_text: &str,
22    installed: &HashSet<String>,
23    provided: &HashSet<String>,
24) -> SandboxInfo {
25    let (depends, makedepends, checkdepends, optdepends) = parse_srcinfo_deps(srcinfo_text);
26
27    // Analyze each dependency against host environment
28    let depends_delta = analyze_dependencies(&depends, installed, provided);
29    let makedepends_delta = analyze_dependencies(&makedepends, installed, provided);
30    let checkdepends_delta = analyze_dependencies(&checkdepends, installed, provided);
31    let optdepends_delta = analyze_dependencies(&optdepends, installed, provided);
32
33    SandboxInfo {
34        package_name: package_name.to_string(),
35        depends: depends_delta,
36        makedepends: makedepends_delta,
37        checkdepends: checkdepends_delta,
38        optdepends: optdepends_delta,
39    }
40}
41
42/// What: Analyze package dependencies from PKGBUILD content.
43///
44/// Inputs:
45/// - `package_name`: AUR package name.
46/// - `pkgbuild_text`: PKGBUILD content.
47/// - `installed`: Set of installed package names.
48/// - `provided`: Set of package names provided by installed packages.
49///
50/// Output:
51/// - `SandboxInfo` with dependency deltas.
52pub(super) fn analyze_package_from_pkgbuild(
53    package_name: &str,
54    pkgbuild_text: &str,
55    installed: &HashSet<String>,
56    provided: &HashSet<String>,
57) -> SandboxInfo {
58    let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild_text);
59
60    // Analyze each dependency against host environment
61    let depends_delta = analyze_dependencies(&depends, installed, provided);
62    let makedepends_delta = analyze_dependencies(&makedepends, installed, provided);
63    let checkdepends_delta = analyze_dependencies(&checkdepends, installed, provided);
64    let optdepends_delta = analyze_dependencies(&optdepends, installed, provided);
65
66    SandboxInfo {
67        package_name: package_name.to_string(),
68        depends: depends_delta,
69        makedepends: makedepends_delta,
70        checkdepends: checkdepends_delta,
71        optdepends: optdepends_delta,
72    }
73}
74
75/// What: Analyze dependencies against the host environment.
76///
77/// Inputs:
78/// - `deps`: Vector of dependency specifications.
79/// - `installed`: Set of installed package names.
80///
81/// Output:
82/// - Vector of `DependencyDelta` entries showing status of each dependency.
83///
84/// Details:
85/// - Skips local packages entirely.
86pub(super) fn analyze_dependencies(
87    deps: &[String],
88    installed: &HashSet<String>,
89    provided: &HashSet<String>,
90) -> Vec<DependencyDelta> {
91    deps.iter()
92        .filter_map(|dep_spec| {
93            // Extract package name (may include version requirements)
94            let pkg_name = extract_package_name(dep_spec);
95            // Check if package is installed or provided by an installed package
96            let is_installed = crate::logic::deps::is_package_installed_or_provided(
97                &pkg_name, installed, provided,
98            );
99
100            // Skip local packages - they're not relevant for sandbox analysis
101            if is_installed && is_local_package(&pkg_name) {
102                return None;
103            }
104
105            // Try to get installed version
106            let installed_version = if is_installed {
107                crate::logic::deps::get_installed_version(&pkg_name).ok()
108            } else {
109                None
110            };
111
112            // Check if version requirement is satisfied
113            let version_satisfied = installed_version
114                .as_ref()
115                .is_some_and(|version| crate::logic::deps::version_satisfies(version, dep_spec));
116
117            Some(DependencyDelta {
118                name: dep_spec.clone(),
119                is_installed,
120                installed_version,
121                version_satisfied,
122            })
123        })
124        .collect()
125}
126
127/// What: Extract package name from a dependency specification.
128///
129/// Inputs:
130/// - `dep_spec`: Dependency specification (e.g., "foo>=1.2", "bar", "baz: description").
131///
132/// Output:
133/// - Package name without version requirements or description.
134#[must_use]
135pub fn extract_package_name(dep_spec: &str) -> String {
136    // Handle optdepends format: "package: description"
137    let name = dep_spec
138        .find(':')
139        .map_or_else(|| dep_spec, |colon_pos| &dep_spec[..colon_pos]);
140
141    // Remove version operators: >=, <=, ==, >, <
142    name.trim()
143        .split(">=")
144        .next()
145        .unwrap_or(name)
146        .split("<=")
147        .next()
148        .unwrap_or(name)
149        .split("==")
150        .next()
151        .unwrap_or(name)
152        .split('>')
153        .next()
154        .unwrap_or(name)
155        .split('<')
156        .next()
157        .unwrap_or(name)
158        .trim()
159        .to_string()
160}
161
162/// What: Check if a package is a local package.
163///
164/// Inputs:
165/// - `name`: Package name to check.
166///
167/// Output:
168/// - `true` if the package is local, `false` otherwise.
169fn is_local_package(name: &str) -> bool {
170    let output = Command::new("pacman")
171        .args(["-Qi", name])
172        .env("LC_ALL", "C")
173        .env("LANG", "C")
174        .stdin(Stdio::null())
175        .stdout(Stdio::piped())
176        .stderr(Stdio::piped())
177        .output();
178
179    match output {
180        Ok(output) if output.status.success() => {
181            let text = String::from_utf8_lossy(&output.stdout);
182            // Look for "Repository" field in pacman -Qi output
183            for line in text.lines() {
184                if line.starts_with("Repository")
185                    && let Some(colon_pos) = line.find(':')
186                {
187                    let repo = line[colon_pos + 1..].trim().to_lowercase();
188                    return repo == "local" || repo.is_empty();
189                }
190            }
191        }
192        _ => {
193            // If we can't determine, assume it's not local
194            return false;
195        }
196    }
197
198    false
199}
200
201/// What: Get the set of installed packages.
202///
203/// Inputs:
204/// - None.
205///
206/// Output:
207/// - Set of installed package names.
208pub(super) fn get_installed_packages() -> std::collections::HashSet<String> {
209    crate::logic::deps::get_installed_packages()
210}