Skip to main content

zoi_cli/pkg/
helper.rs

1use crate::pkg::{
2    install::{manifest, resolver::InstallNode},
3    local, types,
4};
5use anyhow::{Result, anyhow};
6use mlua::{Function, Lua, Table};
7use sha2::{Digest, Sha256, Sha512};
8use std::fs::File;
9use std::io::Read;
10use std::path::{Path, PathBuf};
11
12pub fn elevate_install_node(cmd: &crate::cmd::helper::ElevateInstallNodeCommand) -> Result<()> {
13    let content = std::fs::read_to_string(&cmd.node_json)?;
14    let node: InstallNode = serde_json::from_str(&content)?;
15
16    let pkg = &node.pkg;
17    let handle = &node.registry_handle;
18    let sub_packages_vec = node.sub_package.clone().map(|s| vec![s]);
19
20    let installed_files = crate::pkg::install::pkg_install::run(
21        &cmd.archive,
22        Some(pkg.scope),
23        handle,
24        Some(&node.version),
25        cmd.yes,
26        sub_packages_vec,
27        cmd.link_bins,
28        None,
29    )?;
30
31    if let types::InstallReason::Dependency { ref parent } = node.reason {
32        let package_dir = local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
33        local::add_dependent(&package_dir, parent)?;
34    }
35
36    let manifest = manifest::create_manifest(
37        pkg,
38        node.reason.clone(),
39        node.dependencies.clone(),
40        Some(cmd.install_method.clone()),
41        installed_files,
42        handle,
43        node.repo_type.clone(),
44        &node.chosen_options,
45        &node.chosen_optionals,
46        node.sub_package.clone(),
47    )?;
48
49    local::write_manifest(&manifest)?;
50    local::persist_package_source(&manifest, Path::new(&node.source))?;
51
52    Ok(())
53}
54
55pub fn elevate_uninstall(cmd: &crate::cmd::helper::ElevateUninstallCommand) -> Result<()> {
56    let content = std::fs::read_to_string(&cmd.manifest_json)?;
57    let manifest: types::InstallManifest = serde_json::from_str(&content)?;
58
59    let handle = &manifest.registry_handle;
60    let scope = manifest.scope;
61    let package_dir = local::get_package_dir(scope, handle, &manifest.repo, &manifest.name)?;
62    let version_dir = package_dir.join(&manifest.version);
63
64    let pkg_lua_path = local::get_package_source_path(&manifest)?;
65    let mut pkg_opt = None;
66    if pkg_lua_path.exists() {
67        let path_str = pkg_lua_path
68            .to_str()
69            .ok_or_else(|| anyhow!("Package path contains invalid UTF-8"))?;
70        if let Ok(p) = crate::pkg::lua::parser::parse_lua_package(
71            path_str,
72            Some(&manifest.version),
73            Some(manifest.scope),
74            true,
75        ) {
76            pkg_opt = Some(p);
77        }
78    }
79
80    if let Some(pkg) = &pkg_opt
81        && let Some(hooks) = &pkg.hooks
82    {
83        let _ = crate::pkg::hooks::run_hooks(
84            hooks,
85            crate::pkg::hooks::HookType::PreRemove,
86            manifest.scope,
87        );
88    }
89
90    if pkg_lua_path.exists() {
91        let lua = Lua::new();
92        if crate::pkg::lua::functions::setup_lua_environment(
93            &lua,
94            &crate::pkg::utils::get_platform()?,
95            Some(&manifest.version),
96            pkg_lua_path.to_str(),
97            None,
98            None,
99            None,
100            manifest.sub_package.as_deref(),
101            Some(manifest.scope),
102            None,
103            true,
104        )
105        .is_ok()
106        {
107            let lua_code = std::fs::read_to_string(&pkg_lua_path)?;
108            if lua.load(&lua_code).exec().is_ok() {
109                if let Ok(uninstall_fn) = lua.globals().get::<Function>("uninstall") {
110                    let _ = uninstall_fn.call::<()>(());
111                }
112
113                if let Ok(uninstall_ops) = lua.globals().get::<Table>("__ZoiUninstallOperations") {
114                    for op in uninstall_ops.sequence_values::<Table>() {
115                        if let Ok(op) = op
116                            && let Ok(op_type) = op.get::<String>("op")
117                            && op_type == "zrm"
118                        {
119                            let mut path_to_remove: String = op.get("path").unwrap_or_default();
120                            path_to_remove = path_to_remove
121                                .replace("${pkgstore}", &version_dir.to_string_lossy());
122                            if let Some(home_dir) = crate::pkg::utils::get_user_home() {
123                                path_to_remove = path_to_remove
124                                    .replace("${usrhome}", &home_dir.to_string_lossy());
125                            }
126                            path_to_remove = path_to_remove.replace(
127                                "${usrroot}",
128                                &crate::pkg::sysroot::apply_sysroot(PathBuf::from("/"))
129                                    .to_string_lossy(),
130                            );
131
132                            let path = std::path::PathBuf::from(path_to_remove);
133                            if path.exists() {
134                                if path.is_dir() {
135                                    let _ = std::fs::remove_dir_all(path);
136                                } else {
137                                    let _ = std::fs::remove_file(path);
138                                }
139                            }
140                        }
141                    }
142                }
143            }
144        }
145    }
146
147    if let Some(bins) = &manifest.bins {
148        let bin_root = if cfg!(target_os = "windows") {
149            Path::new("C:\\ProgramData\\zoi\\pkgs\\bin").to_path_buf()
150        } else {
151            Path::new("/usr/local/bin").to_path_buf()
152        };
153
154        for bin in bins {
155            let symlink_path = bin_root.join(bin);
156            if symlink_path.is_symlink() || symlink_path.exists() {
157                let _ = std::fs::remove_file(&symlink_path);
158            }
159        }
160    }
161
162    for file_path_str in &manifest.installed_files {
163        let file_path = Path::new(file_path_str);
164        if file_path.exists() {
165            if file_path.is_dir() {
166                let _ = std::fs::remove_dir_all(file_path);
167            } else {
168                let _ = std::fs::remove_file(file_path);
169            }
170        }
171    }
172
173    let manifest_filename = if let Some(sub) = &manifest.sub_package {
174        format!("manifest-{}.yaml", sub)
175    } else {
176        "manifest.yaml".to_string()
177    };
178    let manifest_path = version_dir.join(manifest_filename);
179    if manifest_path.exists() {
180        std::fs::remove_file(manifest_path)?;
181    }
182
183    if version_dir.exists() && std::fs::read_dir(&version_dir)?.next().is_none() {
184        std::fs::remove_dir_all(version_dir)?;
185    }
186
187    if package_dir.exists() {
188        let _ = crate::pkg::service::cleanup_service(&manifest.name, scope);
189        if let Ok(mut entries) = std::fs::read_dir(&package_dir)
190            && entries.next().is_none()
191        {
192            std::fs::remove_dir_all(package_dir)?;
193        }
194    }
195
196    let parent_id = format!(
197        "#{}@{}/{}@{}",
198        manifest.registry_handle, manifest.repo, manifest.name, manifest.version
199    );
200    for dep_str in &manifest.installed_dependencies {
201        if let Ok(dep) = crate::pkg::dependencies::parse_dependency_string(dep_str)
202            && dep.manager == "zoi"
203        {
204            let dep_req = crate::pkg::resolve::parse_source_string(dep.package)?;
205            let dep_matches =
206                crate::pkg::local::find_installed_manifests_matching(&dep_req, scope)?;
207            if dep_matches.len() == 1 {
208                let dep_manifest = &dep_matches[0];
209                if let Ok(dep_pkg_dir) = crate::pkg::local::get_package_dir(
210                    dep_manifest.scope,
211                    &dep_manifest.registry_handle,
212                    &dep_manifest.repo,
213                    &dep_manifest.name,
214                ) {
215                    let _ = crate::pkg::local::remove_dependent(&dep_pkg_dir, &parent_id);
216                }
217            }
218        }
219    }
220
221    if let Some(pkg) = &pkg_opt
222        && let Some(hooks) = &pkg.hooks
223    {
224        let _ = crate::pkg::hooks::run_hooks(
225            hooks,
226            crate::pkg::hooks::HookType::PostRemove,
227            manifest.scope,
228        );
229    }
230
231    Ok(())
232}
233
234pub enum HashType {
235    Sha512,
236    Sha256,
237}
238
239fn update_digest_from_reader<R: Read, D: Digest>(reader: &mut R, hasher: &mut D) -> Result<()> {
240    let mut buffer = [0; 8192];
241    loop {
242        let bytes_read = reader.read(&mut buffer)?;
243        if bytes_read == 0 {
244            break;
245        }
246        hasher.update(&buffer[..bytes_read]);
247    }
248    Ok(())
249}
250
251pub fn get_hash(source: &str, hash_type: HashType) -> Result<String> {
252    let mut hasher_sha512 = Sha512::new();
253    let mut hasher_sha256 = Sha256::new();
254
255    if source.starts_with("http://") || source.starts_with("https://") {
256        let client = crate::pkg::utils::get_http_client()?;
257        let mut response = client.get(source).send()?;
258        if !response.status().is_success() {
259            return Err(anyhow!(
260                "Failed to download file from URL: {}",
261                response.status()
262            ));
263        }
264        match hash_type {
265            HashType::Sha512 => {
266                update_digest_from_reader(&mut response, &mut hasher_sha512)?;
267            }
268            HashType::Sha256 => {
269                update_digest_from_reader(&mut response, &mut hasher_sha256)?;
270            }
271        }
272    } else {
273        let mut file = File::open(source)?;
274        match hash_type {
275            HashType::Sha512 => {
276                update_digest_from_reader(&mut file, &mut hasher_sha512)?;
277            }
278            HashType::Sha256 => {
279                update_digest_from_reader(&mut file, &mut hasher_sha256)?;
280            }
281        }
282    };
283
284    let hash = match hash_type {
285        HashType::Sha512 => hex::encode(hasher_sha512.finalize()),
286        HashType::Sha256 => hex::encode(hasher_sha256.finalize()),
287    };
288
289    Ok(hash)
290}
291
292pub mod validate {
293    use anyhow::{Result, anyhow};
294    use colored::Colorize;
295    use std::path::Path;
296
297    pub fn run(file: &Path) -> Result<()> {
298        if !file.exists() {
299            return Err(anyhow!("File does not exist: {}", file.display()));
300        }
301
302        let content = std::fs::read_to_string(file)?;
303        let file_name = file
304            .file_name()
305            .and_then(|n| n.to_str())
306            .unwrap_or_default();
307
308        println!("{} Validating {}...", "::".bold().blue(), file.display());
309
310        if file_name == "registries.json" {
311            let _: crate::pkg::purl::CentralDbSpec = serde_json::from_str(&content)
312                .map_err(|e| anyhow!("Invalid registries.json spec: {}", e))?;
313            println!(
314                "{} file is a valid registries.json spec.",
315                "OK".bold().green()
316            );
317        } else if file_name == "repo.yaml" || file_name == "repo.yml" {
318            let _: crate::pkg::types::RepoConfig = serde_yaml::from_str(&content)
319                .map_err(|e| anyhow!("Invalid repo.yaml spec: {}", e))?;
320            println!("{} file is a valid repo.yaml spec.", "OK".bold().green());
321        } else if file_name == "advisories.json" {
322            let _: crate::pkg::types::AdvisoryRegistry = serde_json::from_str(&content)
323                .map_err(|e| anyhow!("Invalid advisories.json spec: {}", e))?;
324            println!(
325                "{} file is a valid advisories.json spec.",
326                "OK".bold().green()
327            );
328        } else if file_name == "packages.json" {
329            let _: crate::pkg::purl::RegistryIndex = serde_json::from_str(&content)
330                .map_err(|e| anyhow!("Invalid packages.json spec: {}", e))?;
331            println!(
332                "{} file is a valid packages.json spec.",
333                "OK".bold().green()
334            );
335        } else if file_name.ends_with(".sec.yaml") || file_name.ends_with(".sec.yml") {
336            let _: crate::pkg::types::Advisory = serde_yaml::from_str(&content)
337                .map_err(|e| anyhow!("Invalid security advisory (.sec.yaml) spec: {}", e))?;
338            println!("{} file is a valid .sec.yaml spec.", "OK".bold().green());
339        } else {
340            if file.extension().and_then(|e| e.to_str()) == Some("json") {
341                if serde_json::from_str::<crate::pkg::purl::CentralDbSpec>(&content).is_ok() {
342                    println!("{} file matches registries.json spec.", "OK".bold().green());
343                } else if serde_json::from_str::<crate::pkg::types::AdvisoryRegistry>(&content)
344                    .is_ok()
345                {
346                    println!("{} file matches advisories.json spec.", "OK".bold().green());
347                } else if serde_json::from_str::<crate::pkg::purl::RegistryIndex>(&content).is_ok()
348                {
349                    println!("{} file matches packages.json spec.", "OK".bold().green());
350                } else {
351                    return Err(anyhow!(
352                        "File does not match any known Zoi JSON spec (registries.json, advisories.json, or packages.json)"
353                    ));
354                }
355            } else if file.extension().and_then(|e| e.to_str()) == Some("yaml")
356                || file.extension().and_then(|e| e.to_str()) == Some("yml")
357            {
358                if serde_yaml::from_str::<crate::pkg::types::RepoConfig>(&content).is_ok() {
359                    println!("{} file matches repo.yaml spec.", "OK".bold().green());
360                } else if serde_yaml::from_str::<crate::pkg::types::Advisory>(&content).is_ok() {
361                    println!("{} file matches .sec.yaml spec.", "OK".bold().green());
362                } else {
363                    return Err(anyhow!(
364                        "File does not match any known Zoi YAML spec (repo.yaml or .sec.yaml)"
365                    ));
366                }
367            } else {
368                return Err(anyhow!(
369                    "Unsupported file extension. Please provide a .json or .yaml file"
370                ));
371            }
372        }
373
374        Ok(())
375    }
376}