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