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 = crate::pkg::utils::get_system_bin_dir();
184
185 for bin in bins {
186 let symlink_path = bin_root.join(bin);
187 if symlink_path.is_symlink() || symlink_path.exists() {
188 let _ = std::fs::remove_file(&symlink_path);
189 }
190 }
191 }
192
193 for file_path_str in &manifest.installed_files {
194 let expanded = crate::pkg::utils::expand_placeholders(
197 file_path_str,
198 &version_dir,
199 scope
200 )?;
201 let file_path = PathBuf::from(expanded);
202 let Ok(meta) = std::fs::symlink_metadata(&file_path) else {
205 continue;
206 };
207 if meta.file_type().is_symlink() {
208 let _ = std::fs::remove_file(&file_path);
209 } else if meta.is_dir() {
210 if std::fs::read_dir(&file_path)
213 .is_ok_and(|mut entries| entries.next().is_none())
214 {
215 let _ = std::fs::remove_dir(&file_path);
216 }
217 } else {
218 let _ = std::fs::remove_file(&file_path);
219 }
220 }
221
222 let manifest_filename = if let Some(sub) = &manifest.sub_package {
223 format!("manifest-{sub}.yaml")
224 } else {
225 "manifest.yaml".to_string()
226 };
227 let manifest_path = version_dir.join(manifest_filename);
228 if manifest_path.exists() {
229 std::fs::remove_file(manifest_path)?;
230 }
231
232 if version_dir.exists() && std::fs::read_dir(&version_dir)?.next().is_none()
233 {
234 std::fs::remove_dir_all(version_dir)?;
235 }
236
237 if package_dir.exists() {
238 let _ = crate::pkg::service::cleanup_service(&manifest.name, scope);
239 if let Ok(mut entries) = std::fs::read_dir(&package_dir)
240 && entries.next().is_none()
241 {
242 std::fs::remove_dir_all(package_dir)?;
243 }
244 }
245
246 let parent_id = format!(
247 "#{}@{}/{}@{}",
248 manifest.registry_handle,
249 manifest.repo,
250 manifest.name,
251 manifest.version
252 );
253 for dep_str in &manifest.installed_dependencies {
254 if let Ok(dep) =
255 crate::pkg::dependencies::parse_dependency_string(dep_str)
256 && dep.manager == "zoi"
257 {
258 let dep_req =
259 crate::pkg::resolve::parse_source_string(dep.package)?;
260 let dep_matches =
261 crate::pkg::local::find_installed_manifests_matching(
262 &dep_req, scope
263 )?;
264 if dep_matches.len() == 1
265 && let Some(dep_manifest) = dep_matches.first()
266 && let Ok(dep_pkg_dir) = crate::pkg::local::get_package_dir(
267 dep_manifest.scope,
268 &dep_manifest.registry_handle,
269 &dep_manifest.repo,
270 &dep_manifest.name
271 )
272 {
273 let _ = crate::pkg::local::remove_dependent(
274 &dep_pkg_dir,
275 &parent_id
276 );
277 }
278 }
279 }
280
281 if let Some(pkg) = &pkg_opt
282 && let Some(hooks) = &pkg.hooks
283 {
284 let _ = crate::pkg::hooks::run_hooks(
285 hooks,
286 crate::pkg::hooks::HookType::PostRemove,
287 manifest.scope
288 );
289 }
290
291 Ok(())
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub enum HashType {
297 Sha512,
299 Sha256
301}
302
303fn update_digest_from_reader<R: Read, D: Digest>(
305 reader: &mut R,
306 hasher: &mut D
307) -> Result<()> {
308 let mut buffer = [0; 8192];
309 loop {
310 let bytes_read = reader.read(&mut buffer)?;
311 if bytes_read == 0 {
312 break;
313 }
314 if let Some(chunk) = buffer.get(..bytes_read) {
315 hasher.update(chunk);
316 }
317 }
318 Ok(())
319}
320
321pub fn get_hash(source: &str, hash_type: HashType) -> Result<String> {
328 let mut hasher_sha512 = Sha512::new();
329 let mut hasher_sha256 = Sha256::new();
330
331 if source.starts_with("http://") || source.starts_with("https://") {
332 let client = crate::pkg::utils::get_http_client()?;
333 let mut response = client.get(source).send()?;
334 if !response.status().is_success() {
335 let status = response.status();
336 return Err(anyhow!("Failed to download file from URL: {status}"));
337 }
338 match hash_type {
339 HashType::Sha512 => {
340 update_digest_from_reader(&mut response, &mut hasher_sha512)?;
341 }
342 HashType::Sha256 => {
343 update_digest_from_reader(&mut response, &mut hasher_sha256)?;
344 }
345 }
346 } else {
347 let mut file = File::open(source)?;
348 match hash_type {
349 HashType::Sha512 => {
350 update_digest_from_reader(&mut file, &mut hasher_sha512)?;
351 }
352 HashType::Sha256 => {
353 update_digest_from_reader(&mut file, &mut hasher_sha256)?;
354 }
355 }
356 }
357
358 let hash = match hash_type {
359 HashType::Sha512 => hex::encode(hasher_sha512.finalize()),
360 HashType::Sha256 => hex::encode(hasher_sha256.finalize())
361 };
362
363 Ok(hash)
364}
365
366pub mod validate {
368 use std::path::Path;
369
370 use anyhow::{Result, anyhow};
371 use colored::Colorize;
372
373 pub fn run(file: &Path) -> Result<()> {
380 if !file.exists() {
381 let path = file.display();
382 return Err(anyhow!("File does not exist: {path}"));
383 }
384
385 let content = std::fs::read_to_string(file)?;
386 let file_name = file
387 .file_name()
388 .and_then(|n| n.to_str())
389 .unwrap_or_default();
390
391 let path = file.display();
392 println!("{} Validating {path}...", "::".bold().blue());
393
394 if file_name == "registries.json" {
395 let _: crate::pkg::purl::CentralDbSpec =
396 serde_json::from_str(&content).map_err(|e| {
397 anyhow!("Invalid registries.json spec: {e}")
398 })?;
399 println!(
400 "{} file is a valid registries.json spec.",
401 "OK".bold().green()
402 );
403 } else if file_name == "repo.yaml" || file_name == "repo.yml" {
404 let _: crate::pkg::types::RepoConfig =
405 serde_yaml::from_str(&content)
406 .map_err(|e| anyhow!("Invalid repo.yaml spec: {e}"))?;
407 println!("{} file is a valid repo.yaml spec.", "OK".bold().green());
408 } else if file_name == "advisories.json" {
409 let _: crate::pkg::types::AdvisoryRegistry =
410 serde_json::from_str(&content).map_err(|e| {
411 anyhow!("Invalid advisories.json spec: {e}")
412 })?;
413 println!(
414 "{} file is a valid advisories.json spec.",
415 "OK".bold().green()
416 );
417 } else if file_name == "packages.json" {
418 let _: crate::pkg::purl::RegistryIndex =
419 serde_json::from_str(&content)
420 .map_err(|e| anyhow!("Invalid packages.json spec: {e}"))?;
421 println!(
422 "{} file is a valid packages.json spec.",
423 "OK".bold().green()
424 );
425 } else if file_name.ends_with(".sec.yaml")
426 || file_name.ends_with(".sec.yml")
427 {
428 let _: crate::pkg::types::Advisory = serde_yaml::from_str(&content)
429 .map_err(|e| {
430 anyhow!("Invalid security advisory (.sec.yaml) spec: {e}")
431 })?;
432 println!("{} file is a valid .sec.yaml spec.", "OK".bold().green());
433 } else if file.extension().and_then(|e| e.to_str()) == Some("json") {
434 if serde_json::from_str::<crate::pkg::purl::CentralDbSpec>(&content).is_ok() {
435 println!("{} file matches registries.json spec.", "OK".bold().green());
436 } else if serde_json::from_str::<crate::pkg::types::AdvisoryRegistry>(&content)
437 .is_ok()
438 {
439 println!("{} file matches advisories.json spec.", "OK".bold().green());
440 } else if serde_json::from_str::<crate::pkg::purl::RegistryIndex>(&content).is_ok()
441 {
442 println!("{} file matches packages.json spec.", "OK".bold().green());
443 } else {
444 return Err(anyhow!(
445 "File does not match any known Zoi JSON spec (registries.json, advisories.json, or packages.json)"
446 ));
447 }
448 } else if file.extension().and_then(|e| e.to_str()) == Some("yaml")
449 || file.extension().and_then(|e| e.to_str()) == Some("yml")
450 {
451 if serde_yaml::from_str::<crate::pkg::types::RepoConfig>(&content)
452 .is_ok()
453 {
454 println!(
455 "{} file matches repo.yaml spec.",
456 "OK".bold().green()
457 );
458 } else if serde_yaml::from_str::<crate::pkg::types::Advisory>(
459 &content
460 )
461 .is_ok()
462 {
463 println!(
464 "{} file matches .sec.yaml spec.",
465 "OK".bold().green()
466 );
467 } else {
468 return Err(anyhow!(
469 "File does not match any known Zoi YAML spec (repo.yaml \
470 or .sec.yaml)"
471 ));
472 }
473 } else {
474 return Err(anyhow!(
475 "Unsupported file extension. Please provide a .json or .yaml \
476 file"
477 ));
478 }
479
480 Ok(())
481 }
482}