Skip to main content

zoi_resolver/
mini_resolve.rs

1//! Optimized dependency resolution for Zoi Mini.
2//!
3//! This module provides a fast, lightweight resolution path that uses a
4//! pre-compiled JSON index instead of full registry checkouts.
5
6use std::collections::HashMap;
7
8use anyhow::{Result, anyhow};
9use colored::Colorize;
10use serde::{Deserialize, Serialize};
11pub use zoi_core::types::MiniVulnerability;
12
13/// Returns the default package revision ("1").
14fn default_revision() -> String {
15    "1".to_string()
16}
17
18/// A simplified version of package metadata used in Zoi Mini's remote index.
19///
20/// This index allows Zoi Mini to perform fast lookups and vulnerability checks
21/// without downloading individual `.pkg.lua` files or cloning entire
22/// registries.
23#[derive(Debug, Serialize, Deserialize, Clone)]
24pub struct MiniPackageIndex {
25    /// The repository where the package is located.
26    pub repo: String,
27    /// The type of repository (e.g. official, unofficial).
28    pub repo_type: String,
29    /// The current version of the package.
30    pub version: String,
31    /// The revision of the package version.
32    #[serde(default = "default_revision")]
33    pub revision: String,
34    /// A short description of the package.
35    pub description: String,
36    /// A list of sub-packages provided by this package.
37    #[serde(default, deserialize_with = "deserialize_sub_packages")]
38    pub sub_packages: Option<Vec<String>>,
39    /// Known vulnerabilities for this package.
40    pub vuln: Option<Vec<MiniVulnerability>>
41}
42
43/// Custom deserializer for sub-packages to handle potential null or non-array
44/// values gracefully.
45fn deserialize_sub_packages<'de, D>(
46    deserializer: D
47) -> Result<Option<Vec<String>>, D::Error>
48where
49    D: serde::Deserializer<'de>
50{
51    let v: serde_json::Value = serde::Deserialize::deserialize(deserializer)?;
52    if v.is_array() {
53        Ok(serde_json::from_value(v).ok())
54    } else {
55        Ok(None)
56    }
57}
58
59/// A mapping of package names to their metadata in the Mini index.
60#[derive(Debug, Serialize, Deserialize, Clone)]
61pub struct MiniRegistryIndex {
62    /// The collection of packages in the index.
63    pub packages: HashMap<String, MiniPackageIndex>
64}
65
66/// Returns the set (default) built-in registry to use for Mini resolution.
67///
68/// # Errors
69///
70/// Returns an error if no built-in registry is marked as the set registry or
71/// if the built-in definitions are invalid.
72fn set_registry_info() -> Result<(String, String)> {
73    let reg = zoi_core::builtin::registry::get_set()?.ok_or_else(|| {
74        anyhow!(
75            "No set registry is defined in the built-in registries. Please \
76             reinstall Zoi."
77        )
78    })?;
79    Ok((reg.git, reg.branch))
80}
81
82/// Fetches the optimized JSON index from the set built-in registry.
83///
84/// This index is the backbone of Zoi Mini, providing a pre-resolved mapping
85/// of package names to their current versions and metadata.
86///
87/// # Errors
88///
89/// Returns an error if the index cannot be fetched or parsed.
90pub fn fetch_registry_index() -> Result<MiniRegistryIndex> {
91    let (git, branch) = set_registry_info()?;
92    let url = zoi_purl::construct_raw_url(&git, &branch, "packages.json")?;
93    let client = zoi_core::utils::get_http_client()?;
94    let response = client.get(url).send()?;
95    if !response.status().is_success() {
96        return Err(anyhow!(
97            "Failed to fetch packages.json from registry: {}",
98            response.status()
99        ));
100    }
101    let index: MiniRegistryIndex = response.json()?;
102    Ok(index)
103}
104
105/// Fetches the repository configuration from the set built-in registry.
106///
107/// # Errors
108///
109/// Returns an error if the configuration cannot be fetched or parsed.
110pub fn fetch_registry_config() -> Result<zoi_core::types::RepoConfig> {
111    let (git, branch) = set_registry_info()?;
112    let url = zoi_purl::construct_raw_url(&git, &branch, "repo.yaml")?;
113    let client = zoi_core::utils::get_http_client()?;
114    let response = client.get(url).send()?;
115    if !response.status().is_success() {
116        return Err(anyhow!(
117            "Failed to fetch repo.yaml from registry: {}",
118            response.status()
119        ));
120    }
121    let content = response.text()?;
122    let config: zoi_core::types::RepoConfig = serde_yaml::from_str(&content)?;
123    Ok(config)
124}
125
126/// Returns the URL to download a package's `.pkg.lua` file from the set
127/// built-in registry.
128pub fn get_package_lua_url(repo: &str, name: &str) -> String {
129    let file_path = if repo.is_empty() {
130        format!("{name}/{name}.pkg.lua")
131    } else {
132        format!("{repo}/{name}/{name}.pkg.lua")
133    };
134    let (git, branch) = set_registry_info().unwrap_or_else(|_| {
135        (
136            "https://gitlab.com/zillowe/zillwen/zusty/zoidberg".to_string(),
137            "main".to_string()
138        )
139    });
140    zoi_purl::construct_raw_url(&git, &branch, &file_path).unwrap_or(file_path)
141}
142
143/// Scans the package metadata for known security advisories.
144///
145/// Returns `true` if the package is safe to install, or if the user
146/// explicitly chooses to bypass a security warning.
147///
148/// # Errors
149///
150/// Returns an error if the version cannot be parsed or if the user confirmation
151/// fails.
152pub fn check_vulnerabilities(
153    pkg_name: &str,
154    pkg_index: &MiniPackageIndex,
155    version: &str
156) -> Result<bool> {
157    let Some(vulns) = &pkg_index.vuln else {
158        return Ok(true);
159    };
160
161    let target_version =
162        semver::Version::parse(version.trim_start_matches('v'))
163            .map_err(|e| anyhow!("Failed to parse version {version}: {e}"))?;
164
165    let mut affected = Vec::new();
166
167    for vuln in vulns {
168        if let Ok(req) = semver::VersionReq::parse(&vuln.affected_range)
169            && req.matches(&target_version)
170        {
171            affected.push(vuln);
172        }
173    }
174
175    if affected.is_empty() {
176        return Ok(true);
177    }
178
179    println!("\n{}", "SECURITY WARNING".red().bold());
180    for vuln in affected {
181        println!(
182            "Package {} v{} is known to be vulnerable:",
183            pkg_name.cyan().bold(),
184            version.red()
185        );
186        println!(
187            "[{}] {} (Severity: {})",
188            vuln.id.dimmed(),
189            vuln.summary,
190            vuln.severity.to_uppercase()
191        );
192        if let Some(fixed) = &vuln.fixed_in {
193            println!("Fixed in version: {}", fixed.green());
194        }
195        println!();
196    }
197
198    Ok(zoi_core::utils::ask_for_confirmation(
199        "Do you want to continue with the installation anyway?",
200        false
201    ))
202}