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}