zoi/project/
verify.rs

1use crate::{
2    pkg::{hash, local, types},
3    project,
4};
5use anyhow::{Result, anyhow};
6use std::collections::HashMap;
7
8pub fn run() -> Result<()> {
9    println!("Verifying project integrity with zoi.lock...");
10
11    let lockfile = project::lockfile::read_zoi_lock()?;
12    let installed_packages = local::get_installed_packages()?
13        .into_iter()
14        .filter(|p| p.scope == types::Scope::Project)
15        .collect::<Vec<_>>();
16
17    let mut lockfile_pkgs_map = HashMap::new();
18    for (reg_key, pkgs) in &lockfile.details {
19        for (short_id, detail) in pkgs {
20            let full_id = format!("{}{}", reg_key, short_id);
21            lockfile_pkgs_map.insert(full_id, detail);
22        }
23    }
24
25    let mut installed_pkgs_map = HashMap::new();
26    for installed_pkg in &installed_packages {
27        let name_with_sub = if let Some(sub) = &installed_pkg.sub_package {
28            format!("{}:{}", installed_pkg.name, sub)
29        } else {
30            installed_pkg.name.clone()
31        };
32        let full_id = format!(
33            "#{}@{}/{}",
34            installed_pkg.registry_handle, installed_pkg.repo, name_with_sub
35        );
36        installed_pkgs_map.insert(full_id, installed_pkg);
37    }
38
39    for (full_id, lock_detail) in &lockfile_pkgs_map {
40        if let Some(installed_pkg) = installed_pkgs_map.get(full_id) {
41            if installed_pkg.version != lock_detail.version {
42                return Err(anyhow!(
43                    "Version mismatch for '{}': lockfile requires v{}, but v{} is installed.",
44                    full_id,
45                    lock_detail.version,
46                    installed_pkg.version
47                ));
48            }
49
50            let parts: Vec<&str> = full_id.split('@').collect();
51            let registry_handle = parts[0].strip_prefix('#').unwrap();
52            let repo_and_name_with_sub = parts[1];
53
54            if let Some(last_slash_idx) = repo_and_name_with_sub.rfind('/') {
55                let (repo, name_with_sub) = repo_and_name_with_sub.split_at(last_slash_idx);
56                let name_with_sub = &name_with_sub[1..];
57
58                let name = if let Some(colon_idx) = name_with_sub.rfind(':') {
59                    &name_with_sub[..colon_idx]
60                } else {
61                    name_with_sub
62                };
63
64                let package_dir =
65                    local::get_package_dir(types::Scope::Project, registry_handle, repo, name)?;
66                let latest_dir = package_dir.join("latest");
67                if !latest_dir.exists() {
68                    return Err(anyhow!(
69                        "Package '{}' is missing from the project's .zoi directory, though it is in the manifest.",
70                        full_id
71                    ));
72                }
73                let integrity = hash::calculate_dir_hash(&latest_dir)?;
74                if integrity != lock_detail.integrity {
75                    return Err(anyhow!(
76                        "Integrity check failed for '{}'. The installed files do not match the lockfile. Your project is in an inconsistent state.",
77                        full_id
78                    ));
79                }
80            } else {
81                return Err(anyhow!(
82                    "Invalid package ID format in lockfile: {}",
83                    full_id
84                ));
85            }
86        } else {
87            return Err(anyhow!(
88                "Package '{}' from zoi.lock is not installed.",
89                full_id
90            ));
91        }
92    }
93
94    for full_id in installed_pkgs_map.keys() {
95        if !lockfile_pkgs_map.contains_key(full_id) {
96            return Err(anyhow!(
97                "Package '{}' is installed in the project but is not in zoi.lock.",
98                full_id
99            ));
100        }
101    }
102
103    println!("Project is consistent with zoi.lock.");
104    Ok(())
105}