use crate::pkg::{cache, install::resolver::InstallNode, types};
use crate::utils;
use anyhow::{Result, anyhow};
use colored::*;
use home;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use semver::{Version, VersionReq};
use sha2::{Digest, Sha512};
use std::collections::HashSet;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tar::Archive;
use tempfile::Builder;
use walkdir::WalkDir;
use zstd::stream::read::Decoder as ZstdDecoder;
pub fn send_telemetry(
event: &str,
pkg: &types::Package,
registry_handle: &str,
install_type: Option<&str>,
) {
match crate::pkg::telemetry::posthog_capture_event(
event,
pkg,
env!("CARGO_PKG_VERSION"),
registry_handle,
install_type,
) {
Ok(true) => println!("{} telemetry sent", "Info:".green()),
Ok(false) => (),
Err(e) => eprintln!("{} telemetry failed: {}", "Warning:".yellow(), e),
}
}
pub fn display_updates(pkg: &types::Package, yes: bool) -> Result<bool> {
if let Some(updates) = &pkg.updates {
if updates.is_empty() {
return Ok(true);
}
println!("\n{}", "Important Updates:".bold().yellow());
for update in updates {
let type_str = match update.update_type {
types::UpdateType::Change => "Change".blue(),
types::UpdateType::Vulnerability => "Vulnerability".red().bold(),
types::UpdateType::Update => "Update".green(),
};
println!(" - [{}] {}", type_str, update.message);
}
if !utils::ask_for_confirmation("\nDo you want to continue?", yes) {
return Ok(false);
}
}
Ok(true)
}
pub fn get_conflicts(
pkg: &types::Package,
installed_packages: &[types::InstallManifest],
) -> Result<Vec<String>> {
let mut conflict_messages = Vec::new();
if let Some(conflicts_with) = &pkg.conflicts {
for conflict_pkg_name in conflicts_with {
let is_zoi_conflict = installed_packages.iter().any(|p| {
&p.name == conflict_pkg_name
&& (p.name != pkg.name || p.sub_package != pkg.sub_package)
});
if is_zoi_conflict {
conflict_messages.push(format!(
"Package '{}' conflicts with installed package '{}'.",
pkg.name, conflict_pkg_name
));
} else if utils::command_exists(conflict_pkg_name) {
conflict_messages.push(format!(
"Package '{}' conflicts with existing command '{}' on your system.",
pkg.name, conflict_pkg_name
));
}
}
}
if let Some(bins_provided) = &pkg.bins {
for bin in bins_provided {
for installed_pkg in installed_packages {
if installed_pkg.name == pkg.name && installed_pkg.sub_package == pkg.sub_package {
continue;
}
if let Some(installed_bins) = &installed_pkg.bins
&& installed_bins.contains(bin)
{
conflict_messages.push(format!(
"Binary '{}' provided by '{}' is already provided by installed package '{}'.",
bin, pkg.name, installed_pkg.name
));
}
}
}
}
if let Some(provides) = &pkg.provides {
for p in provides {
for installed_pkg in installed_packages {
if installed_pkg.name == pkg.name && installed_pkg.sub_package == pkg.sub_package {
continue;
}
if let Some(installed_provides) = &installed_pkg.provides
&& installed_provides.contains(p)
{
conflict_messages.push(format!(
"Virtual package '{}' provided by '{}' is already provided by installed package '{}'.",
p, pkg.name, installed_pkg.name
));
}
}
}
}
Ok(conflict_messages)
}
pub fn check_for_conflicts(packages_to_install: &[&types::Package], yes: bool) -> Result<()> {
let installed_packages = crate::pkg::local::get_installed_packages()?;
let mut all_conflict_messages = HashSet::new();
for pkg in packages_to_install {
let conflicts = get_conflicts(pkg, &installed_packages)?;
all_conflict_messages.extend(conflicts);
}
if !all_conflict_messages.is_empty() {
println!("\n{}", "Conflict Detected:".red().bold());
for msg in &all_conflict_messages {
println!("- {}", msg);
}
if !utils::ask_for_confirmation(
"\nDo you want to continue with the installation anyway?",
yes,
) {
return Err(anyhow!("Operation aborted by user due to conflicts."));
}
}
Ok(())
}
pub fn check_for_vulnerabilities(
graph: &super::resolver::DependencyGraph,
yes: bool,
) -> Result<()> {
let mut all_vulnerabilities = Vec::new();
for node in graph.nodes.values() {
if let Ok(advisories) = crate::pkg::db::get_advisories_for_package(
&node.registry_handle,
&node.pkg.name,
node.sub_package.as_deref(),
) {
for adv in advisories {
if let Ok(version) = Version::parse(&node.version)
&& let Ok(req) = VersionReq::parse(&adv.affected_range)
&& req.matches(&version)
{
all_vulnerabilities.push((
adv,
node.version.clone(),
node.pkg.name.clone(),
node.sub_package.clone(),
));
}
}
}
}
if !all_vulnerabilities.is_empty() {
println!("\n{}", "SECURITY WARNING".red().bold());
for (adv, version, pkg_name, sub_pkg) in &all_vulnerabilities {
let display_name = if let Some(sub) = sub_pkg {
format!("{}:{}", pkg_name, sub)
} else {
pkg_name.clone()
};
println!(
"Package {} v{} is known to be vulnerable:",
display_name.cyan().bold(),
version.red()
);
println!(
"[{}] {} (Severity: {})",
adv.id.dimmed(),
adv.summary,
match adv.severity {
types::Severity::Low => "Low".blue(),
types::Severity::Medium => "Medium".yellow(),
types::Severity::High => "High".red(),
types::Severity::Critical => "Critical".magenta().bold(),
}
);
if let Some(fixed) = &adv.fixed_in {
println!("Fixed in version: {}", fixed.green());
}
println!();
}
let config = crate::pkg::config::read_config()?;
if config.policy.advisory_enforcement_unoverridable {
return Err(anyhow!(
"Installation blocked by system policy due to security vulnerabilities."
));
}
if !utils::ask_for_confirmation(
"Do you want to continue with the installation anyway?",
yes,
) {
return Err(anyhow!(
"Operation aborted by user due to security vulnerabilities."
));
}
}
Ok(())
}
pub fn get_filename_from_url(url: &str) -> &str {
url.split('/').next_back().unwrap_or_default()
}
pub fn download_file_with_progress(
url: &str,
dest_path: &Path,
pb_override: Option<&ProgressBar>,
expected_size: Option<u64>,
) -> Result<()> {
if url.starts_with("http://") {
let msg = format!("downloading over insecure HTTP: {}", url);
if pb_override.is_none() {
println!("{}: {}", "Warning:".yellow(), msg);
}
}
let pb_style = ProgressStyle::default_bar()
.template("{spinner:.green} {msg:30.cyan.bold} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {elapsed_precise})")?
.progress_chars("=>-");
let mut internal_pb = None;
let pb = if let Some(p) = pb_override {
p.set_style(pb_style.clone());
p.set_length(expected_size.unwrap_or(0));
p.set_message(format!("Downloading {}", get_filename_from_url(url)));
p
} else {
let p = ProgressBar::new(expected_size.unwrap_or(0));
p.set_style(pb_style);
p.set_message(format!("Downloading {}", get_filename_from_url(url)));
internal_pb = Some(p);
internal_pb
.as_ref()
.expect("internal_pb should be set if not using pb_override")
};
let client = crate::utils::get_http_client()?;
let mut attempt = 0u32;
let mut partial_size = 0;
if dest_path.exists() {
partial_size = dest_path.metadata()?.len();
}
let mut request = client.get(url);
if partial_size > 0 {
let msg = format!("Resuming download from byte {}", partial_size);
pb.set_message(msg);
request = request.header("Range", format!("bytes={}-", partial_size));
}
let response = loop {
attempt += 1;
match request
.try_clone()
.ok_or_else(|| anyhow!("Failed to clone request"))?
.send()
{
Ok(resp) => break resp,
Err(e) => {
if attempt < 3 {
let msg = format!("Download failed ({}). Retrying...", e);
pb.set_message(msg);
crate::utils::retry_backoff_sleep(attempt);
continue;
} else {
return Err(anyhow!(
"Failed to download '{}' after {} attempts: {}",
url,
attempt,
e
));
}
}
}
};
let mut is_resumed = false;
if response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
is_resumed = true;
} else if response.status().is_success() {
partial_size = 0;
} else {
return Err(anyhow!(
"Failed to download (HTTP {}): {}",
response.status(),
url
));
}
let total_size = if let Some(s) = expected_size {
s
} else {
partial_size + response.content_length().unwrap_or(0)
};
pb.set_length(total_size);
pb.set_position(partial_size);
pb.set_message(format!("Downloading {}", get_filename_from_url(url)));
let mut dest_file = if is_resumed {
std::fs::OpenOptions::new().append(true).open(dest_path)?
} else {
File::create(dest_path)?
};
let mut stream = response;
let mut buffer = [0; 8192];
loop {
let bytes_read = stream.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
dest_file.write_all(&buffer[..bytes_read])?;
pb.inc(bytes_read as u64);
}
if let Some(p) = internal_pb {
p.finish_and_clear();
println!("Downloaded {}", get_filename_from_url(url));
}
Ok(())
}
pub fn verify_file_hash(
file_path: &Path,
expected_hash: &str,
pb: Option<&ProgressBar>,
) -> Result<bool> {
let mut file = File::open(file_path)?;
let mut hasher = Sha512::new();
std::io::copy(&mut file, &mut hasher)?;
let actual_hash = hex::encode(hasher.finalize());
let expected_clean = expected_hash.trim().to_lowercase();
let actual_clean = actual_hash.trim().to_lowercase();
let result = actual_clean == expected_clean;
if result {
let msg = format!(
"{} Hash verified: {}",
"::".bold().blue(),
expected_clean[..12].dimmed()
);
if let Some(p) = pb {
p.println(msg);
} else {
println!("{}", msg);
}
} else {
let mut msg = format!("{}\n", "Hash verification failed!".red().bold());
msg.push_str(&format!(" Expected: {}\n", expected_clean.yellow()));
msg.push_str(&format!(" Actual: {}\n", actual_clean.cyan()));
msg.push_str(&format!(
" Lengths: Expected={}, Actual={}",
expected_clean.len(),
actual_clean.len()
));
if expected_clean.len() != 128 || actual_clean.len() != 128 {
msg.push_str(&format!(
"\n {} One of the hashes is not 128 characters long (SHA-512 requirement).",
"Note:".bold().yellow()
));
}
if let Some(p) = pb {
p.println(msg);
} else {
println!("{}", msg);
}
}
Ok(result)
}
pub fn get_remote_file_list(url: &str) -> Result<Vec<String>> {
if crate::pkg::offline::is_offline() {
return Ok(Vec::new());
}
let client = crate::utils::get_http_client()?;
let resp = client
.get(url)
.send()
.map_err(|e| anyhow!("Failed to fetch files list from {}: {}", url, e))?
.text()
.map_err(|e| anyhow!("Failed to read files list content from {}: {}", url, e))?;
Ok(resp
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
pub fn check_file_conflicts(
graph: &super::resolver::DependencyGraph,
yes: bool,
m: &MultiProgress,
) -> Result<()> {
let mut all_conflicts = HashSet::new();
let installed_packages = crate::pkg::local::get_installed_packages()?;
for node in graph.nodes.values() {
let request = match crate::pkg::resolve::parse_source_string(&node.source) {
Ok(req) => req,
Err(_) => continue,
};
let owned_files: HashSet<String> = installed_packages
.iter()
.find(|p| {
p.name == node.pkg.name
&& p.sub_package.as_deref() == request.sub_package.as_deref()
})
.map(|p| p.installed_files.iter().cloned().collect())
.unwrap_or_default();
let mut conflicts_for_this_pkg = Vec::new();
if let Ok(Some(info)) = find_prebuilt_info(node) {
let mut file_list = None;
if let Some(files_url) = &info.files_url
&& let Ok(list) = get_remote_file_list(files_url)
{
file_list = Some(list);
}
if let Some(list) = file_list {
let conflicts =
get_conflicts_from_list(list, &node.pkg, request.sub_package.as_deref())?;
conflicts_for_this_pkg.extend(conflicts);
} else {
let archive_filename = info.final_url.split('/').next_back().unwrap_or_default();
let archive_cache_root = match cache::get_archive_cache_root() {
Ok(path) => path,
Err(_) => continue,
};
let archive_path = archive_cache_root.join(archive_filename);
if archive_path.exists()
&& let Ok(conflicts) = get_file_conflicts_from_archive(
&archive_path,
&node.pkg,
request.sub_package.as_deref(),
)
{
conflicts_for_this_pkg.extend(conflicts);
}
}
}
for conflict in conflicts_for_this_pkg {
if !owned_files.contains(&conflict) {
all_conflicts.insert(format!(
"File '{}' from package '{}' already exists on filesystem.",
conflict, node.pkg.name
));
}
}
}
if !all_conflicts.is_empty() {
m.println(format!("\n{}", "File Conflict Detected:".red().bold()))?;
for msg in &all_conflicts {
m.println(format!("- {}", msg))?;
}
if !utils::ask_for_confirmation(
"\nDo you want to overwrite these files and continue with the installation?",
yes,
) {
return Err(anyhow!("Operation aborted by user due to file conflicts."));
}
}
Ok(())
}
pub fn get_conflicts_from_list(
list: Vec<String>,
pkg: &types::Package,
sub_package_to_check: Option<&str>,
) -> Result<Vec<String>> {
let mut conflicts = Vec::new();
let sub_prefix = if let Some(sub) = sub_package_to_check {
format!("data/{}/", sub)
} else {
"data/".to_string()
};
for path_in_archive in list {
if !path_in_archive.starts_with(&sub_prefix) {
continue;
}
let rel_to_data = &path_in_archive[sub_prefix.len()..];
let dest_path = if let Some(stripped) = rel_to_data.strip_prefix("usrroot/") {
if pkg.scope != types::Scope::System {
continue;
}
Some(crate::pkg::sysroot::apply_sysroot(
PathBuf::from("/").join(stripped),
))
} else if let Some(stripped) = rel_to_data.strip_prefix("usrhome/") {
home::home_dir().map(|h| h.join(stripped))
} else {
None
};
if let Some(p) = dest_path
&& p.exists()
&& p.is_file()
{
conflicts.push(p.to_string_lossy().to_string());
}
}
Ok(conflicts)
}
pub fn get_file_conflicts_from_archive(
archive_path: &Path,
pkg: &types::Package,
sub_package_to_check: Option<&str>,
) -> Result<Vec<String>> {
let file = File::open(archive_path)?;
let decoder = ZstdDecoder::new(file)?;
let mut archive = Archive::new(decoder);
let temp_dir = Builder::new().prefix("zoi-conflict-check-").tempdir()?;
archive.unpack(temp_dir.path())?;
let mut conflicts = Vec::new();
let data_dir = temp_dir.path().join("data");
if !data_dir.exists() {
return Ok(conflicts);
}
let subs_to_check = if let Some(sub) = sub_package_to_check {
vec![sub.to_string()]
} else {
vec!["".to_string()]
};
for sub in subs_to_check {
let sub_data_dir = if sub.is_empty() {
data_dir.clone()
} else {
data_dir.join(&sub)
};
if !sub_data_dir.exists() {
continue;
}
let usrroot_src = sub_data_dir.join("usrroot");
if usrroot_src.exists() && pkg.scope == types::Scope::System {
let root_dest = crate::pkg::sysroot::apply_sysroot(PathBuf::from("/"));
for entry in WalkDir::new(&usrroot_src)
.into_iter()
.filter_map(|e| e.ok())
.skip(1)
{
if entry.file_type().is_file() {
let relative_path = entry.path().strip_prefix(&usrroot_src)?;
let dest_path = root_dest.join(relative_path);
if dest_path.exists() {
conflicts.push(dest_path.to_string_lossy().to_string());
}
}
}
}
let usrhome_src = sub_data_dir.join("usrhome");
if usrhome_src.exists()
&& let Some(home_dest) = home::home_dir()
{
for entry in WalkDir::new(&usrhome_src)
.into_iter()
.filter_map(|e| e.ok())
.skip(1)
{
if entry.file_type().is_file() {
let relative_path = entry.path().strip_prefix(&usrhome_src)?;
let dest_path = home_dest.join(relative_path);
if dest_path.exists() {
conflicts.push(dest_path.to_string_lossy().to_string());
}
}
}
}
}
Ok(conflicts)
}
pub fn get_expected_hash(hash_url: &str, filename: Option<&str>) -> Result<String> {
if crate::pkg::offline::is_offline() {
return Ok(String::new());
}
let client = crate::utils::get_http_client()?;
let resp = client
.get(hash_url)
.send()
.map_err(|e| anyhow!("Failed to fetch hash file from {}: {}", hash_url, e))?
.text()
.map_err(|e| anyhow!("Failed to read hash file content from {}: {}", hash_url, e))?;
let is_sha512 = |s: &str| s.len() == 128 && s.chars().all(|c| c.is_ascii_hexdigit());
if let Some(target_file) = filename {
for line in resp.lines() {
if line.contains(target_file) {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(hash) = parts.iter().find(|&&p| is_sha512(p)) {
return Ok(hash.to_string());
}
}
}
}
for word in resp.split_whitespace() {
if is_sha512(word) {
return Ok(word.to_string());
}
}
Ok(resp
.split_whitespace()
.next()
.unwrap_or_default()
.to_string())
}
pub fn get_expected_size(size_url: &str) -> Result<(u64, u64)> {
if crate::pkg::offline::is_offline() {
return Ok((0, 0));
}
let client = crate::utils::get_http_client()?;
let resp = client
.get(size_url)
.send()
.map_err(|e| anyhow!("Failed to fetch size file from {}: {}", size_url, e))?
.text()
.map_err(|e| anyhow!("Failed to read size file content from {}: {}", size_url, e))?;
let mut download_size = 0;
let mut installed_size = 0;
let mut found_fields = false;
for line in resp.lines() {
if let Some((key, val)) = line.split_once(':')
&& let Ok(num) = val.trim().parse::<u64>()
{
match key.trim() {
"down" => {
download_size = num;
found_fields = true;
}
"install" => {
installed_size = num;
found_fields = true;
}
_ => {}
}
}
}
if !found_fields && let Ok(num) = resp.trim().parse::<u64>() {
download_size = num;
}
Ok((download_size, installed_size))
}
pub fn resolve_url_placeholders(
url: &str,
pkg_name: &str,
repo: &str,
version: &str,
platform: &str,
) -> String {
let (os, arch) = (
platform.split('-').next().unwrap_or_default(),
platform.split('-').nth(1).unwrap_or_default(),
);
url.replace("{os}", os)
.replace("{arch}", arch)
.replace("{version}", version)
.replace("{repo}", repo)
.replace("{name}", pkg_name)
.replace("{platform}", platform)
}
pub fn find_prebuilt_info(node: &InstallNode) -> Result<Option<types::PrebuiltInfo>> {
let pkg = &node.pkg;
let platform = crate::utils::get_platform()?;
let db_path = crate::pkg::resolve::get_db_root()?;
let repo_db_path = db_path.join(&node.registry_handle);
if let Ok(repo_config) = crate::pkg::config::read_repo_config(&repo_db_path) {
let mut pkg_links_to_try = Vec::new();
if let Some(main_pkg) = repo_config.pkg.iter().find(|p| p.link_type == "main") {
pkg_links_to_try.push(main_pkg.clone());
}
pkg_links_to_try.extend(
repo_config
.pkg
.iter()
.filter(|p| p.link_type == "mirror")
.cloned(),
);
if let Some(pkg_link) = pkg_links_to_try.into_iter().next() {
let final_url_base = resolve_url_placeholders(
&pkg_link.url,
&pkg.name,
&pkg.repo,
&node.version,
&platform,
);
let final_url = if final_url_base.ends_with(".pkg.tar.zst") {
final_url_base
} else {
let archive_filename =
format!("{}-{}-{}.pkg.tar.zst", pkg.name, &node.version, platform);
format!(
"{}/{}",
final_url_base.trim_end_matches('/'),
archive_filename
)
};
let pgp_url = pkg_link.pgp.as_ref().map(|url| {
resolve_url_placeholders(url, &pkg.name, &pkg.repo, &node.version, &platform)
});
let hash_url = pkg_link.hash.as_ref().map(|url| {
resolve_url_placeholders(url, &pkg.name, &pkg.repo, &node.version, &platform)
});
let size_url = pkg_link.size.as_ref().map(|url| {
resolve_url_placeholders(url, &pkg.name, &pkg.repo, &node.version, &platform)
});
let files_url = pkg_link.files.as_ref().map(|url| {
resolve_url_placeholders(url, &pkg.name, &pkg.repo, &node.version, &platform)
});
return Ok(Some(types::PrebuiltInfo {
final_url,
pgp_url,
hash_url,
size_url,
files_url,
}));
}
}
Ok(None)
}