1use crate::sysroot::apply_sysroot;
2use crate::types::{Config, Registry, RepoConfig};
3use crate::utils::{get_db_root, get_user_home};
4use anyhow::{Result, anyhow};
5use colored::*;
6use serde_yaml::Value;
7use std::collections::{HashMap, HashSet};
8use std::fs;
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11
12pub fn get_default_registry() -> String {
16 option_env!("ZOI_DEFAULT_REGISTRY")
17 .unwrap_or("community")
18 .to_string()
19}
20
21pub fn get_builtin_authorities() -> Vec<String> {
23 let auth_str = option_env!("ZOI_BUILTIN_AUTHORITIES").unwrap_or_default();
24 if auth_str.is_empty() {
25 Vec::new()
26 } else {
27 auth_str.split(',').map(|s| s.to_string()).collect()
28 }
29}
30
31fn get_system_config_path() -> Result<PathBuf> {
32 if cfg!(target_os = "windows") {
33 Ok(apply_sysroot(PathBuf::from(
34 "C:\\ProgramData\\zoi\\config.yaml",
35 )))
36 } else {
37 Ok(apply_sysroot(PathBuf::from("/etc/zoi/config.yaml")))
38 }
39}
40
41fn get_user_config_path() -> Result<PathBuf> {
42 let home_dir = get_user_home().ok_or_else(|| anyhow!("Could not find home directory."))?;
43 Ok(apply_sysroot(home_dir.join(".zoi"))
44 .join("pkgs")
45 .join("config.yaml"))
46}
47
48fn get_project_config_path() -> Result<PathBuf> {
49 let current_dir = std::env::current_dir()?;
50 Ok(apply_sysroot(current_dir.join(".zoi"))
51 .join("pkgs")
52 .join("config.yaml"))
53}
54
55fn get_git_root() -> Result<PathBuf> {
56 let home_dir = get_user_home().ok_or_else(|| anyhow!("Could not find home directory."))?;
57 Ok(apply_sysroot(home_dir.join(".zoi"))
58 .join("pkgs")
59 .join("git"))
60}
61
62fn get_remote_policy_cache_path() -> Result<PathBuf> {
63 if cfg!(target_os = "windows") {
64 Ok(apply_sysroot(PathBuf::from(
65 "C:\\ProgramData\\zoi\\policy.cache.yaml",
66 )))
67 } else {
68 Ok(apply_sysroot(PathBuf::from("/etc/zoi/policy.cache.yaml")))
69 }
70}
71
72fn read_yaml_value(path: &Path) -> Result<Value> {
73 if !path.exists() {
74 return Ok(Value::Null);
75 }
76 let content = fs::read_to_string(path)?;
77 serde_yaml::from_str(&content).map_err(Into::into)
78}
79
80fn read_config_from_path(path: &Path) -> Result<Config> {
81 if !path.exists() {
82 return Ok(Config::default());
83 }
84 let content = fs::read_to_string(path)?;
85 serde_yaml::from_str(&content).map_err(Into::into)
86}
87
88pub fn read_config() -> Result<Config> {
98 let system_val = read_yaml_value(&get_system_config_path()?)?;
99 let user_val = read_yaml_value(&get_user_config_path()?)?;
100 let project_val = read_yaml_value(&get_project_config_path()?)?;
101
102 let mut system_cfg: Config = serde_yaml::from_value(system_val.clone()).unwrap_or_default();
103 let user_cfg: Config = serde_yaml::from_value(user_val.clone()).unwrap_or_default();
104 let project_cfg: Config = serde_yaml::from_value(project_val.clone()).unwrap_or_default();
105
106 if let Ok(cache_path) = get_remote_policy_cache_path()
107 && cache_path.exists()
108 && let Ok(cache_content) = fs::read_to_string(&cache_path)
109 && let Ok(remote_policy) = serde_yaml::from_str::<crate::types::Policy>(&cache_content)
110 {
111 merge_policies(&mut system_cfg.policy, &remote_policy);
112 }
113
114 let system_policy = system_cfg.policy.clone();
115 let mut merged_cfg = Config {
116 policy: system_policy.clone(),
117 ..Default::default()
118 };
119
120 merged_cfg.repos = system_cfg.repos;
121 if !system_policy.repos_unoverridable {
122 merged_cfg.repos.extend(user_cfg.repos);
123 merged_cfg.repos.extend(project_cfg.repos);
124 }
125 merged_cfg.repos.sort();
126 merged_cfg.repos.dedup();
127
128 merged_cfg.added_registries = system_cfg.added_registries;
129 if !system_policy.added_registries_unoverridable {
130 merged_cfg
131 .added_registries
132 .extend(user_cfg.added_registries);
133 merged_cfg
134 .added_registries
135 .extend(project_cfg.added_registries);
136 }
137 let mut seen_registries = HashSet::new();
138 merged_cfg
139 .added_registries
140 .retain(|r| seen_registries.insert(r.url.clone()));
141
142 merged_cfg.git_repos = system_cfg.git_repos;
143 if !system_policy.git_repos_unoverridable {
144 merged_cfg.git_repos.extend(user_cfg.git_repos);
145 merged_cfg.git_repos.extend(project_cfg.git_repos);
146 }
147 merged_cfg.git_repos.sort();
148 merged_cfg.git_repos.dedup();
149
150 merged_cfg.package_managers = project_cfg
151 .package_managers
152 .or(user_cfg.package_managers)
153 .or(system_cfg.package_managers);
154 merged_cfg.native_package_manager = project_cfg
155 .native_package_manager
156 .or(user_cfg.native_package_manager)
157 .or(system_cfg.native_package_manager);
158 merged_cfg.registry = project_cfg
159 .registry
160 .or(user_cfg.registry)
161 .or(system_cfg.registry);
162
163 merged_cfg.remote_policy = system_cfg.remote_policy;
164
165 if project_val.get("telemetry_enabled").is_some()
166 && !system_policy.telemetry_enabled_unoverridable
167 {
168 merged_cfg.telemetry_enabled = project_cfg.telemetry_enabled;
169 } else if user_val.get("telemetry_enabled").is_some()
170 && !system_policy.telemetry_enabled_unoverridable
171 {
172 merged_cfg.telemetry_enabled = user_cfg.telemetry_enabled;
173 } else {
174 merged_cfg.telemetry_enabled = system_cfg.telemetry_enabled;
175 }
176
177 if project_val.get("audit_log_enabled").is_some()
178 && !system_policy.audit_log_enabled_unoverridable
179 {
180 merged_cfg.audit_log_enabled = project_cfg.audit_log_enabled;
181 } else if user_val.get("audit_log_enabled").is_some()
182 && !system_policy.audit_log_enabled_unoverridable
183 {
184 merged_cfg.audit_log_enabled = user_cfg.audit_log_enabled;
185 } else {
186 merged_cfg.audit_log_enabled = system_cfg.audit_log_enabled;
187 }
188
189 if project_val.get("rollback_enabled").is_some()
190 && !system_policy.rollback_enabled_unoverridable
191 {
192 merged_cfg.rollback_enabled = project_cfg.rollback_enabled;
193 } else if user_val.get("rollback_enabled").is_some()
194 && !system_policy.rollback_enabled_unoverridable
195 {
196 merged_cfg.rollback_enabled = user_cfg.rollback_enabled;
197 } else {
198 merged_cfg.rollback_enabled = system_cfg.rollback_enabled;
199 }
200
201 if project_val.get("default_registry").is_some()
202 && !system_policy.default_registry_unoverridable
203 {
204 merged_cfg.default_registry = project_cfg.default_registry;
205 } else if user_val.get("default_registry").is_some()
206 && !system_policy.default_registry_unoverridable
207 {
208 merged_cfg.default_registry = user_cfg.default_registry;
209 } else {
210 merged_cfg.default_registry = system_cfg.default_registry;
211 }
212
213 if project_val.get("jobs").is_some() && !system_policy.jobs_unoverridable {
214 merged_cfg.jobs = project_cfg.jobs;
215 } else if user_val.get("jobs").is_some() && !system_policy.jobs_unoverridable {
216 merged_cfg.jobs = user_cfg.jobs;
217 } else {
218 merged_cfg.jobs = system_cfg.jobs;
219 }
220
221 if project_val.get("protect_db").is_some() && !system_policy.protect_db_unoverridable {
222 merged_cfg.protect_db = project_cfg.protect_db;
223 } else if user_val.get("protect_db").is_some() && !system_policy.protect_db_unoverridable {
224 merged_cfg.protect_db = user_cfg.protect_db;
225 } else {
226 merged_cfg.protect_db = system_cfg.protect_db;
227 }
228
229 if project_val.get("max_resolution_depth").is_some()
230 && !system_policy.max_resolution_depth_unoverridable
231 {
232 merged_cfg.max_resolution_depth = project_cfg.max_resolution_depth;
233 } else if user_val.get("max_resolution_depth").is_some()
234 && !system_policy.max_resolution_depth_unoverridable
235 {
236 merged_cfg.max_resolution_depth = user_cfg.max_resolution_depth;
237 } else {
238 merged_cfg.max_resolution_depth = system_cfg.max_resolution_depth;
239 }
240
241 if project_val.get("offline_mode").is_some() && !system_policy.offline_mode_unoverridable {
242 merged_cfg.offline_mode = project_cfg.offline_mode;
243 } else if user_val.get("offline_mode").is_some() && !system_policy.offline_mode_unoverridable {
244 merged_cfg.offline_mode = user_cfg.offline_mode;
245 } else {
246 merged_cfg.offline_mode = system_cfg.offline_mode;
247 }
248
249 if project_val
250 .get("policy")
251 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
252 .is_some()
253 && !system_policy.advisory_enforcement_unoverridable
254 {
255 merged_cfg.policy.advisory_enforcement_unoverridable =
256 project_cfg.policy.advisory_enforcement_unoverridable;
257 } else if user_val
258 .get("policy")
259 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
260 .is_some()
261 && !system_policy.advisory_enforcement_unoverridable
262 {
263 merged_cfg.policy.advisory_enforcement_unoverridable =
264 user_cfg.policy.advisory_enforcement_unoverridable;
265 } else {
266 merged_cfg.policy.advisory_enforcement_unoverridable =
267 system_cfg.policy.advisory_enforcement_unoverridable;
268 }
269
270 merged_cfg.versions = system_cfg.versions;
271 merged_cfg.versions.extend(user_cfg.versions);
272 merged_cfg.versions.extend(project_cfg.versions);
273
274 merged_cfg.pkg_dirs = system_cfg.pkg_dirs;
275 if !system_policy.pkg_dirs_unoverridable {
276 merged_cfg.pkg_dirs.extend(user_cfg.pkg_dirs);
277 merged_cfg.pkg_dirs.extend(project_cfg.pkg_dirs);
278 }
279 merged_cfg.pkg_dirs.sort();
280 merged_cfg.pkg_dirs.dedup();
281
282 merged_cfg.cache_mirrors = system_cfg.cache_mirrors;
283 if !system_policy.cache_mirrors_unoverridable {
284 merged_cfg.cache_mirrors.extend(user_cfg.cache_mirrors);
285 merged_cfg.cache_mirrors.extend(project_cfg.cache_mirrors);
286 }
287 merged_cfg.cache_mirrors.sort();
288 merged_cfg.cache_mirrors.dedup();
289
290 if !system_policy.allow_deny_lists_unoverridable {
291 if project_cfg.policy.allowed_licenses.is_some() {
292 merged_cfg.policy.allowed_licenses = project_cfg.policy.allowed_licenses;
293 } else if user_cfg.policy.allowed_licenses.is_some() {
294 merged_cfg.policy.allowed_licenses = user_cfg.policy.allowed_licenses;
295 }
296 if project_cfg.policy.denied_licenses.is_some() {
297 merged_cfg.policy.denied_licenses = project_cfg.policy.denied_licenses;
298 } else if user_cfg.policy.denied_licenses.is_some() {
299 merged_cfg.policy.denied_licenses = user_cfg.policy.denied_licenses;
300 }
301 if project_cfg.policy.allowed_packages.is_some() {
302 merged_cfg.policy.allowed_packages = project_cfg.policy.allowed_packages;
303 } else if user_cfg.policy.allowed_packages.is_some() {
304 merged_cfg.policy.allowed_packages = user_cfg.policy.allowed_packages;
305 }
306 if project_cfg.policy.denied_packages.is_some() {
307 merged_cfg.policy.denied_packages = project_cfg.policy.denied_packages;
308 } else if user_cfg.policy.denied_packages.is_some() {
309 merged_cfg.policy.denied_packages = user_cfg.policy.denied_packages;
310 }
311 if project_cfg.policy.allowed_repos.is_some() {
312 merged_cfg.policy.allowed_repos = project_cfg.policy.allowed_repos;
313 } else if user_cfg.policy.allowed_repos.is_some() {
314 merged_cfg.policy.allowed_repos = user_cfg.policy.allowed_repos;
315 }
316 if project_cfg.policy.denied_repos.is_some() {
317 merged_cfg.policy.denied_repos = project_cfg.policy.denied_repos;
318 } else if user_cfg.policy.denied_repos.is_some() {
319 merged_cfg.policy.denied_repos = user_cfg.policy.denied_repos;
320 }
321 }
322
323 if !system_policy.signature_enforcement_unoverridable {
324 if project_cfg.policy.signature_enforcement.is_some() {
325 merged_cfg.policy.signature_enforcement = project_cfg.policy.signature_enforcement;
326 } else if user_cfg.policy.signature_enforcement.is_some() {
327 merged_cfg.policy.signature_enforcement = user_cfg.policy.signature_enforcement;
328 }
329 }
330
331 if let Some(url) = merged_cfg.registry.take()
332 && merged_cfg.default_registry.is_none()
333 {
334 merged_cfg.default_registry = Some(Registry {
335 handle: String::new(),
336 url,
337 advisory_prefix: None,
338 authorities: None,
339 });
340 }
341
342 if merged_cfg.default_registry.is_none() {
343 merged_cfg.default_registry = Some(Registry {
344 handle: "zoidberg".to_string(),
345 url: get_default_registry(),
346 advisory_prefix: Some("ZSA".to_string()),
347 authorities: Some(get_builtin_authorities()),
348 });
349 } else if let Some(ref mut reg) = merged_cfg.default_registry
350 && reg.url == get_default_registry()
351 && reg.authorities.as_ref().is_none_or(|a| a.is_empty())
352 {
353 let builtin = get_builtin_authorities();
354 if !builtin.is_empty() {
355 reg.authorities = Some(builtin);
356 }
357 }
358
359 if merged_cfg.repos.is_empty()
360 && let Some(reg) = &merged_cfg.default_registry
361 && !reg.handle.is_empty()
362 {
363 let db_root = get_db_root()?;
364 let repo_path = db_root.join(®.handle);
365 if repo_path.join("repo.yaml").exists()
366 && let Ok(repo_config) = read_repo_config(&repo_path)
367 {
368 merged_cfg.repos = repo_config
369 .repos
370 .into_iter()
371 .filter(|r| r.active)
372 .map(|r| r.name)
373 .collect();
374 }
375 }
376
377 if project_val.get("system_generations_limit").is_some()
378 && !system_policy.system_generations_limit_unoverridable
379 {
380 merged_cfg.system_generations_limit = project_cfg.system_generations_limit;
381 } else if user_val.get("system_generations_limit").is_some()
382 && !system_policy.system_generations_limit_unoverridable
383 {
384 merged_cfg.system_generations_limit = user_cfg.system_generations_limit;
385 } else {
386 merged_cfg.system_generations_limit = system_cfg.system_generations_limit;
387 }
388
389 Ok(merged_cfg)
390}
391
392pub fn write_user_config(config: &Config) -> Result<()> {
394 let config_path = get_user_config_path()?;
395 let parent_dir = config_path
396 .parent()
397 .ok_or_else(|| anyhow!("Invalid config path"))?;
398 fs::create_dir_all(parent_dir)?;
399 let content = serde_yaml::to_string(config)?;
400 fs::write(config_path, content)?;
401 Ok(())
402}
403
404pub fn verify_remote_file(url: &str, trusted_keys: &[String]) -> Result<Vec<u8>> {
405 let client = crate::utils::get_http_client()?;
406
407 let response = client.get(url).send()?;
408 if !response.status().is_success() {
409 return Err(anyhow!(
410 "Failed to fetch remote file '{}': {}",
411 url,
412 response.status()
413 ));
414 }
415 let data = response.bytes()?;
416
417 let sig_url = format!("{}.sig", url);
418 let sig_response = client.get(&sig_url).send()?;
419 if !sig_response.status().is_success() {
420 return Err(anyhow!(
421 "Failed to fetch signature for '{}': {}",
422 url,
423 sig_response.status()
424 ));
425 }
426 let sig = sig_response.bytes()?;
427
428 let trusted_certs = crate::pgp::get_certs_by_name_or_fingerprint(trusted_keys)?;
429
430 let temp_dir = tempfile::Builder::new().prefix("zoi-verify-").tempdir()?;
431 let data_path = temp_dir.path().join("data");
432 let sig_path = temp_dir.path().join("sig");
433
434 fs::write(&data_path, &data)?;
435 fs::write(&sig_path, &sig)?;
436
437 crate::pgp::verify_detached_signature_multi_key(&data_path, &sig_path, trusted_certs)?;
438
439 Ok(data.to_vec())
440}
441
442pub fn add_repo(repo_name: &str) -> Result<()> {
443 let mut config = read_config_from_path(&get_user_config_path()?)?;
444 let lower_repo_name = repo_name.to_lowercase();
445 if config.repos.contains(&lower_repo_name) {
446 return Err(anyhow!(
447 "Repository '{}' already exists in user config.",
448 repo_name
449 ));
450 }
451 config.repos.push(lower_repo_name);
452 write_user_config(&config)
453}
454
455pub fn remove_repo(repo_name: &str) -> Result<()> {
456 let mut config = read_config_from_path(&get_user_config_path()?)?;
457 let lower_repo_name = repo_name.to_lowercase();
458 if let Some(pos) = config.repos.iter().position(|r| r == &lower_repo_name) {
459 config.repos.remove(pos);
460 write_user_config(&config)
461 } else {
462 Err(anyhow!(
463 "Repository '{}' not found in user config.",
464 repo_name
465 ))
466 }
467}
468
469pub fn interactive_add_repo() -> Result<()> {
470 let config = read_config()?;
471 let all_repos = get_all_repos()?;
472
473 let available_repos: Vec<_> = all_repos
474 .into_iter()
475 .filter(|r| !config.repos.contains(&r.to_lowercase()))
476 .collect();
477
478 if available_repos.is_empty() {
479 println!("{}", "No new repositories available to add.".yellow());
480 return Ok(());
481 }
482
483 println!("{}", "Available repositories to add:".green());
484 for (i, repo) in available_repos.iter().enumerate() {
485 println!("[{}] {}", i + 1, repo);
486 }
487
488 print!(
489 "\n{}",
490 "Select a repository to add (or 'q' to quit): ".yellow()
491 );
492 io::stdout().flush()?;
493
494 let mut input = String::new();
495 io::stdin().read_line(&mut input)?;
496 let input = input.trim();
497
498 if input == "q" {
499 println!("Aborted.");
500 return Ok(());
501 }
502
503 let choice: usize = match input.parse() {
504 Ok(num) => num,
505 Err(_) => return Err(anyhow!("Invalid input.")),
506 };
507
508 if choice > 0 && choice <= available_repos.len() {
509 let repo_to_add = &available_repos[choice - 1];
510 add_repo(repo_to_add)?;
511 println!("Repository '{}' added successfully.", repo_to_add.green());
512 } else {
513 return Err(anyhow!("Invalid selection."));
514 }
515
516 Ok(())
517}
518
519pub fn get_all_repos() -> Result<Vec<String>> {
520 let db_root = get_db_root()?;
521 let config = read_config()?;
522
523 if let Some(default_reg) = config.default_registry
524 && !default_reg.handle.is_empty()
525 {
526 let default_reg_path = db_root.join(default_reg.handle);
527 if default_reg_path.join("repo.yaml").exists() {
528 let repo_config = read_repo_config(&default_reg_path)?;
529 return Ok(repo_config.repos.into_iter().map(|r| r.name).collect());
530 }
531 }
532
533 Ok(Vec::new())
534}
535
536pub fn clone_git_repo(url: &str) -> Result<()> {
537 let git_root = get_git_root()?;
538 fs::create_dir_all(&git_root)?;
539 let repo_name = url
540 .trim_end_matches('/')
541 .split('/')
542 .next_back()
543 .unwrap_or("repo")
544 .trim_end_matches(".git");
545 let target = git_root.join(repo_name);
546 if target.exists() {
547 return Err(anyhow!(
548 "Git repo '{}' already exists at {}",
549 repo_name,
550 target.display()
551 ));
552 }
553 println!("Cloning '{}' into {}...", url.cyan(), target.display());
554 let status = std::process::Command::new("git")
555 .arg("clone")
556 .arg("--depth=1")
557 .arg(url)
558 .arg(&target)
559 .status()?;
560 if !status.success() {
561 return Err(anyhow!("git clone failed"));
562 }
563
564 let mut config = read_config_from_path(&get_user_config_path()?)?;
565 if !config.git_repos.iter().any(|repo_url| repo_url == url) {
566 config.git_repos.push(url.to_string());
567 write_user_config(&config)?;
568 }
569
570 println!(
571 "Cloned git repo as '{}' (use with '#git@{}/<pkg>')",
572 repo_name.green(),
573 repo_name
574 );
575 Ok(())
576}
577
578pub fn list_git_repos() -> Result<Vec<String>> {
579 let git_root = get_git_root()?;
580 if !git_root.exists() {
581 return Ok(Vec::new());
582 }
583
584 let mut repos = Vec::new();
585 for entry in fs::read_dir(git_root)? {
586 let entry = entry?;
587 if entry.path().is_dir() {
588 repos.push(entry.file_name().to_string_lossy().into_owned());
589 }
590 }
591 repos.sort();
592 Ok(repos)
593}
594
595pub fn remove_git_repo(repo_name: &str) -> Result<()> {
596 let git_root = get_git_root()?;
597 let target = git_root.join(repo_name);
598 if !target.exists() {
599 return Err(anyhow!("Git repository '{}' not found.", repo_name));
600 }
601
602 let mut config = read_config_from_path(&get_user_config_path()?)?;
603 let mut removed = false;
604 config.git_repos.retain(|url| {
605 let name_from_url = url
606 .trim_end_matches('/')
607 .split('/')
608 .next_back()
609 .unwrap_or_default()
610 .trim_end_matches(".git");
611 if name_from_url == repo_name {
612 removed = true;
613 false
614 } else {
615 true
616 }
617 });
618
619 if removed {
620 write_user_config(&config)?;
621 }
622
623 fs::remove_dir_all(&target)?;
624 println!(
625 "Removed git repository '{}' from {}",
626 repo_name.green(),
627 target.display()
628 );
629 Ok(())
630}
631
632pub fn add_cache_mirror(url: &str) -> Result<()> {
633 let mut config = read_config_from_path(&get_user_config_path()?)?;
634 if config.cache_mirrors.iter().any(|existing| existing == url) {
635 return Err(anyhow!("Cache mirror '{}' already exists.", url));
636 }
637 config.cache_mirrors.push(url.to_string());
638 write_user_config(&config)
639}
640
641pub fn remove_cache_mirror(url: &str) -> Result<()> {
642 let mut config = read_config_from_path(&get_user_config_path()?)?;
643 if let Some(pos) = config
644 .cache_mirrors
645 .iter()
646 .position(|existing| existing == url)
647 {
648 config.cache_mirrors.remove(pos);
649 write_user_config(&config)
650 } else {
651 Err(anyhow!("Cache mirror '{}' not found.", url))
652 }
653}
654
655pub fn set_default_registry(url: &str) -> Result<()> {
656 let mut config = read_config_from_path(&get_user_config_path()?)?;
657 config.default_registry = Some(Registry {
658 handle: String::new(),
659 url: url.to_string(),
660 advisory_prefix: None,
661 authorities: None,
662 });
663 write_user_config(&config)
664}
665
666pub fn set_user_default_registry(default_registry: Option<Registry>) -> Result<()> {
667 let mut config = read_config_from_path(&get_user_config_path()?)?;
668 config.default_registry = default_registry;
669 write_user_config(&config)
670}
671
672pub fn add_added_registry(url: &str) -> Result<()> {
673 let mut config = read_config_from_path(&get_user_config_path()?)?;
674 if config.added_registries.iter().any(|r| r.url == url) {
675 return Err(anyhow!("Registry with URL '{}' already exists.", url));
676 }
677 config.added_registries.push(Registry {
678 handle: String::new(),
679 url: url.to_string(),
680 advisory_prefix: None,
681 authorities: None,
682 });
683 write_user_config(&config)
684}
685
686pub fn remove_added_registry(handle_or_url: &str) -> Result<()> {
687 let mut config = read_config_from_path(&get_user_config_path()?)?;
688 if let Some(pos) = config
689 .added_registries
690 .iter()
691 .position(|r| r.handle == handle_or_url || r.url == handle_or_url)
692 {
693 let removed_registry = config.added_registries.remove(pos);
694 if !removed_registry.handle.is_empty() {
695 let db_root = get_db_root()?;
696 let repo_path = db_root.join(removed_registry.handle);
697 if repo_path.exists() {
698 fs::remove_dir_all(repo_path)?;
699 }
700 }
701 write_user_config(&config)
702 } else {
703 Err(anyhow!(
704 "Added registry with handle or URL '{}' not found.",
705 handle_or_url
706 ))
707 }
708}
709
710pub fn read_repo_config(db_path: &Path) -> Result<RepoConfig> {
711 let config_path = db_path.join("repo.yaml");
712 if !config_path.exists() {
713 return Err(anyhow!(
714 "repo.yaml not found in the root of the package database."
715 ));
716 }
717 let content = fs::read_to_string(config_path)?;
718 let config: RepoConfig = serde_yaml::from_str(&content)?;
719 Ok(config)
720}
721
722pub fn read_user_config() -> Result<Config> {
723 read_config_from_path(&get_user_config_path()?)
724}
725
726pub fn update_global_versions(versions: HashMap<String, String>) -> Result<()> {
727 let mut config = read_user_config()?;
728 config.versions.extend(versions);
729 write_user_config(&config)
730}
731
732pub fn sync_remote_policy() -> Result<()> {
737 let config = read_config()?;
738 let Some(remote_cfg) = &config.remote_policy else {
739 return Ok(());
740 };
741
742 println!("{} Syncing remote security policy...", "::".bold().blue());
743
744 if crate::offline::is_offline() {
745 return Err(anyhow!("Cannot sync remote policy in offline mode."));
746 }
747
748 let client = crate::utils::get_http_client()?;
749
750 let policy_content = client.get(&remote_cfg.url).send()?.text()?;
751 let sig_content = client.get(&remote_cfg.signature_url).send()?.bytes()?;
752
753 let trusted_certs = crate::pgp::get_certs_by_name_or_fingerprint(&remote_cfg.trusted_keys)?;
754
755 let temp_dir = tempfile::Builder::new().prefix("zoi-policy-").tempdir()?;
756 let temp_policy_path = temp_dir.path().join("policy.yaml");
757 let temp_sig_path = temp_dir.path().join("policy.yaml.sig");
758
759 fs::write(&temp_policy_path, &policy_content)?;
760 fs::write(&temp_sig_path, &sig_content)?;
761
762 crate::pgp::verify_detached_signature_multi_key(
763 &temp_policy_path,
764 &temp_sig_path,
765 trusted_certs,
766 )?;
767
768 let cache_path = get_remote_policy_cache_path()?;
769 if let Some(parent) = cache_path.parent() {
770 fs::create_dir_all(parent)?;
771 }
772 fs::write(cache_path, policy_content)?;
773
774 println!(
775 "{}",
776 "Remote policy verified and cached successfully.".green()
777 );
778 Ok(())
779}
780
781fn merge_policies(base: &mut crate::types::Policy, remote: &crate::types::Policy) {
787 if remote.repos_unoverridable {
788 base.repos_unoverridable = true;
789 }
790 if remote.telemetry_enabled_unoverridable {
791 base.telemetry_enabled_unoverridable = true;
792 }
793 if remote.audit_log_enabled_unoverridable {
794 base.audit_log_enabled_unoverridable = true;
795 }
796 if remote.rollback_enabled_unoverridable {
797 base.rollback_enabled_unoverridable = true;
798 }
799 if remote.default_registry_unoverridable {
800 base.default_registry_unoverridable = true;
801 }
802 if remote.added_registries_unoverridable {
803 base.added_registries_unoverridable = true;
804 }
805 if remote.git_repos_unoverridable {
806 base.git_repos_unoverridable = true;
807 }
808 if remote.allow_deny_lists_unoverridable {
809 base.allow_deny_lists_unoverridable = true;
810 }
811 if remote.signature_enforcement_unoverridable {
812 base.signature_enforcement_unoverridable = true;
813 }
814 if remote.protect_db_unoverridable {
815 base.protect_db_unoverridable = true;
816 }
817 if remote.max_resolution_depth_unoverridable {
818 base.max_resolution_depth_unoverridable = true;
819 }
820 if remote.offline_mode_unoverridable {
821 base.offline_mode_unoverridable = true;
822 }
823 if remote.pkg_dirs_unoverridable {
824 base.pkg_dirs_unoverridable = true;
825 }
826 if remote.cache_mirrors_unoverridable {
827 base.cache_mirrors_unoverridable = true;
828 }
829 if remote.jobs_unoverridable {
830 base.jobs_unoverridable = true;
831 }
832 if remote.advisory_enforcement_unoverridable {
833 base.advisory_enforcement_unoverridable = true;
834 }
835
836 if let Some(allowed) = &remote.allowed_licenses {
837 base.allowed_licenses
838 .get_or_insert_with(Vec::new)
839 .extend(allowed.clone());
840 }
841 if let Some(denied) = &remote.denied_licenses {
842 base.denied_licenses
843 .get_or_insert_with(Vec::new)
844 .extend(denied.clone());
845 }
846 if let Some(allowed) = &remote.allowed_packages {
847 base.allowed_packages
848 .get_or_insert_with(Vec::new)
849 .extend(allowed.clone());
850 }
851 if let Some(denied) = &remote.denied_packages {
852 base.denied_packages
853 .get_or_insert_with(Vec::new)
854 .extend(denied.clone());
855 }
856 if let Some(allowed) = &remote.allowed_repos {
857 base.allowed_repos
858 .get_or_insert_with(Vec::new)
859 .extend(allowed.clone());
860 }
861 if let Some(denied) = &remote.denied_repos {
862 base.denied_repos
863 .get_or_insert_with(Vec::new)
864 .extend(denied.clone());
865 }
866
867 if let Some(remote_sig) = &remote.signature_enforcement {
868 if let Some(ref mut base_sig) = base.signature_enforcement {
869 if remote_sig.enable {
870 base_sig.enable = true;
871 }
872 base_sig
873 .trusted_keys
874 .extend(remote_sig.trusted_keys.clone());
875 base_sig.trusted_keys.sort();
876 base_sig.trusted_keys.dedup();
877 } else {
878 base.signature_enforcement = Some(remote_sig.clone());
879 }
880 }
881}