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