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