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