1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::io::{self, Write};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use serde_yaml::Value;
9
10use crate::sysroot::apply_sysroot;
11use crate::types::{Config, Registry, RepoConfig};
12use crate::utils::{get_db_root, get_user_config_dir, get_user_data_dir};
13
14pub fn get_default_registry() -> String {
19 option_env!("ZOI_DEFAULT_REGISTRY")
20 .unwrap_or("community")
21 .to_string()
22}
23
24pub fn get_builtin_authorities() -> Vec<String> {
26 let auth_str = option_env!("ZOI_BUILTIN_AUTHORITIES").unwrap_or_default();
27 if auth_str.is_empty() {
28 Vec::new()
29 } else {
30 auth_str.split(',').map(ToString::to_string).collect()
31 }
32}
33
34fn get_system_config_path() -> PathBuf {
36 crate::utils::get_system_config_dir().join("config.yaml")
37}
38
39fn get_user_config_path() -> Result<PathBuf> {
41 Ok(get_user_config_dir()?.join("config.yaml"))
42}
43
44fn get_project_config_path() -> Result<PathBuf> {
46 let current_dir = std::env::current_dir()?;
47 Ok(apply_sysroot(current_dir.join(".zoi"))
48 .join("pkgs")
49 .join("config.yaml"))
50}
51
52fn get_git_root() -> Result<PathBuf> {
54 Ok(get_user_data_dir()?.join("pkgs").join("git"))
55}
56
57fn get_remote_policy_cache_path() -> PathBuf {
59 crate::utils::get_system_cache_dir().join("policy.cache.yaml")
60}
61
62fn read_yaml_value(path: &Path) -> Result<Value> {
64 if !path.exists() {
65 return Ok(Value::Null);
66 }
67 let content = fs::read_to_string(path)?;
68 serde_yaml::from_str(&content).map_err(Into::into)
69}
70
71fn read_config_from_path(path: &Path) -> Result<Config> {
73 if !path.exists() {
74 return Ok(Config::default());
75 }
76 let content = fs::read_to_string(path)?;
77 serde_yaml::from_str(&content).map_err(Into::into)
78}
79
80pub fn read_config() -> Result<Config> {
96 let system_val = read_yaml_value(&get_system_config_path())?;
97 let user_val = read_yaml_value(&get_user_config_path()?)?;
98 let project_val = read_yaml_value(&get_project_config_path()?)?;
99
100 let mut system_cfg: Config =
101 serde_yaml::from_value(system_val.clone()).unwrap_or_default();
102 let user_cfg: Config =
103 serde_yaml::from_value(user_val.clone()).unwrap_or_default();
104 let project_cfg: Config =
105 serde_yaml::from_value(project_val.clone()).unwrap_or_default();
106
107 let cache_path = get_remote_policy_cache_path();
108 if cache_path.exists()
109 && let Ok(cache_content) = fs::read_to_string(&cache_path)
110 && let Ok(remote_policy) =
111 serde_yaml::from_str::<crate::types::Policy>(&cache_content)
112 {
113 merge_policies(&mut system_cfg.policy, &remote_policy);
114 }
115
116 let system_policy = system_cfg.policy.clone();
117 let mut merged_cfg = Config {
118 policy: system_policy.clone(),
119 ..Default::default()
120 };
121
122 merged_cfg.repos = system_cfg.repos;
123 if !system_policy.repos_unoverridable {
124 merged_cfg.repos.extend(user_cfg.repos);
125 merged_cfg.repos.extend(project_cfg.repos);
126 }
127 merged_cfg.repos.sort();
128 merged_cfg.repos.dedup();
129
130 merged_cfg.added_registries = system_cfg.added_registries;
131 if !system_policy.added_registries_unoverridable {
132 merged_cfg
133 .added_registries
134 .extend(user_cfg.added_registries);
135 merged_cfg
136 .added_registries
137 .extend(project_cfg.added_registries);
138 }
139 let mut seen_registries = HashSet::new();
140 merged_cfg
141 .added_registries
142 .retain(|r| seen_registries.insert(r.url.clone()));
143
144 merged_cfg.git_repos = system_cfg.git_repos;
145 if !system_policy.git_repos_unoverridable {
146 merged_cfg.git_repos.extend(user_cfg.git_repos);
147 merged_cfg.git_repos.extend(project_cfg.git_repos);
148 }
149 merged_cfg.git_repos.sort();
150 merged_cfg.git_repos.dedup();
151
152 merged_cfg.package_managers = project_cfg
153 .package_managers
154 .or(user_cfg.package_managers)
155 .or(system_cfg.package_managers);
156 merged_cfg.native_package_manager = project_cfg
157 .native_package_manager
158 .or(user_cfg.native_package_manager)
159 .or(system_cfg.native_package_manager);
160 merged_cfg.registry = project_cfg
161 .registry
162 .or(user_cfg.registry)
163 .or(system_cfg.registry);
164
165 merged_cfg.remote_policy = system_cfg.remote_policy;
166
167 if project_val.get("telemetry_enabled").is_some()
168 && !system_policy.telemetry_enabled_unoverridable
169 {
170 merged_cfg.telemetry_enabled = project_cfg.telemetry_enabled;
171 } else if user_val.get("telemetry_enabled").is_some()
172 && !system_policy.telemetry_enabled_unoverridable
173 {
174 merged_cfg.telemetry_enabled = user_cfg.telemetry_enabled;
175 } else {
176 merged_cfg.telemetry_enabled = system_cfg.telemetry_enabled;
177 }
178
179 if project_val.get("audit_log_enabled").is_some()
180 && !system_policy.audit_log_enabled_unoverridable
181 {
182 merged_cfg.audit_log_enabled = project_cfg.audit_log_enabled;
183 } else if user_val.get("audit_log_enabled").is_some()
184 && !system_policy.audit_log_enabled_unoverridable
185 {
186 merged_cfg.audit_log_enabled = user_cfg.audit_log_enabled;
187 } else {
188 merged_cfg.audit_log_enabled = system_cfg.audit_log_enabled;
189 }
190
191 if project_val.get("rollback_enabled").is_some()
192 && !system_policy.rollback_enabled_unoverridable
193 {
194 merged_cfg.rollback_enabled = project_cfg.rollback_enabled;
195 } else if user_val.get("rollback_enabled").is_some()
196 && !system_policy.rollback_enabled_unoverridable
197 {
198 merged_cfg.rollback_enabled = user_cfg.rollback_enabled;
199 } else {
200 merged_cfg.rollback_enabled = system_cfg.rollback_enabled;
201 }
202
203 if project_val.get("default_registry").is_some()
204 && !system_policy.default_registry_unoverridable
205 {
206 merged_cfg.default_registry = project_cfg.default_registry;
207 } else if user_val.get("default_registry").is_some()
208 && !system_policy.default_registry_unoverridable
209 {
210 merged_cfg.default_registry = user_cfg.default_registry;
211 } else {
212 merged_cfg.default_registry = system_cfg.default_registry;
213 }
214
215 if project_val.get("jobs").is_some() && !system_policy.jobs_unoverridable {
216 merged_cfg.jobs = project_cfg.jobs;
217 } else if user_val.get("jobs").is_some()
218 && !system_policy.jobs_unoverridable
219 {
220 merged_cfg.jobs = user_cfg.jobs;
221 } else {
222 merged_cfg.jobs = system_cfg.jobs;
223 }
224
225 if project_val.get("protect_db").is_some()
226 && !system_policy.protect_db_unoverridable
227 {
228 merged_cfg.protect_db = project_cfg.protect_db;
229 } else if user_val.get("protect_db").is_some()
230 && !system_policy.protect_db_unoverridable
231 {
232 merged_cfg.protect_db = user_cfg.protect_db;
233 } else {
234 merged_cfg.protect_db = system_cfg.protect_db;
235 }
236
237 if project_val.get("max_resolution_depth").is_some()
238 && !system_policy.max_resolution_depth_unoverridable
239 {
240 merged_cfg.max_resolution_depth = project_cfg.max_resolution_depth;
241 } else if user_val.get("max_resolution_depth").is_some()
242 && !system_policy.max_resolution_depth_unoverridable
243 {
244 merged_cfg.max_resolution_depth = user_cfg.max_resolution_depth;
245 } else {
246 merged_cfg.max_resolution_depth = system_cfg.max_resolution_depth;
247 }
248
249 if project_val.get("offline_mode").is_some()
250 && !system_policy.offline_mode_unoverridable
251 {
252 merged_cfg.offline_mode = project_cfg.offline_mode;
253 } else if user_val.get("offline_mode").is_some()
254 && !system_policy.offline_mode_unoverridable
255 {
256 merged_cfg.offline_mode = user_cfg.offline_mode;
257 } else {
258 merged_cfg.offline_mode = system_cfg.offline_mode;
259 }
260
261 if project_val
262 .get("policy")
263 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
264 .is_some()
265 && !system_policy.advisory_enforcement_unoverridable
266 {
267 merged_cfg.policy.advisory_enforcement_unoverridable =
268 project_cfg.policy.advisory_enforcement_unoverridable;
269 } else if user_val
270 .get("policy")
271 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
272 .is_some()
273 && !system_policy.advisory_enforcement_unoverridable
274 {
275 merged_cfg.policy.advisory_enforcement_unoverridable =
276 user_cfg.policy.advisory_enforcement_unoverridable;
277 } else {
278 merged_cfg.policy.advisory_enforcement_unoverridable =
279 system_cfg.policy.advisory_enforcement_unoverridable;
280 }
281
282 merged_cfg.versions = system_cfg.versions;
283 merged_cfg.versions.extend(user_cfg.versions);
284 merged_cfg.versions.extend(project_cfg.versions);
285
286 merged_cfg.pkg_dirs = system_cfg.pkg_dirs;
287 if !system_policy.pkg_dirs_unoverridable {
288 merged_cfg.pkg_dirs.extend(user_cfg.pkg_dirs);
289 merged_cfg.pkg_dirs.extend(project_cfg.pkg_dirs);
290 }
291 merged_cfg.pkg_dirs.sort();
292 merged_cfg.pkg_dirs.dedup();
293
294 merged_cfg.cache_mirrors = system_cfg.cache_mirrors;
295 if !system_policy.cache_mirrors_unoverridable {
296 merged_cfg.cache_mirrors.extend(user_cfg.cache_mirrors);
297 merged_cfg.cache_mirrors.extend(project_cfg.cache_mirrors);
298 }
299 merged_cfg.cache_mirrors.sort();
300 merged_cfg.cache_mirrors.dedup();
301
302 if !system_policy.allow_deny_lists_unoverridable {
303 if project_cfg.policy.allowed_licenses.is_some() {
304 merged_cfg.policy.allowed_licenses =
305 project_cfg.policy.allowed_licenses;
306 } else if user_cfg.policy.allowed_licenses.is_some() {
307 merged_cfg.policy.allowed_licenses =
308 user_cfg.policy.allowed_licenses;
309 }
310 if project_cfg.policy.denied_licenses.is_some() {
311 merged_cfg.policy.denied_licenses =
312 project_cfg.policy.denied_licenses;
313 } else if user_cfg.policy.denied_licenses.is_some() {
314 merged_cfg.policy.denied_licenses = user_cfg.policy.denied_licenses;
315 }
316 if project_cfg.policy.allowed_packages.is_some() {
317 merged_cfg.policy.allowed_packages =
318 project_cfg.policy.allowed_packages;
319 } else if user_cfg.policy.allowed_packages.is_some() {
320 merged_cfg.policy.allowed_packages =
321 user_cfg.policy.allowed_packages;
322 }
323 if project_cfg.policy.denied_packages.is_some() {
324 merged_cfg.policy.denied_packages =
325 project_cfg.policy.denied_packages;
326 } else if user_cfg.policy.denied_packages.is_some() {
327 merged_cfg.policy.denied_packages = user_cfg.policy.denied_packages;
328 }
329 if project_cfg.policy.allowed_repos.is_some() {
330 merged_cfg.policy.allowed_repos = project_cfg.policy.allowed_repos;
331 } else if user_cfg.policy.allowed_repos.is_some() {
332 merged_cfg.policy.allowed_repos = user_cfg.policy.allowed_repos;
333 }
334 if project_cfg.policy.denied_repos.is_some() {
335 merged_cfg.policy.denied_repos = project_cfg.policy.denied_repos;
336 } else if user_cfg.policy.denied_repos.is_some() {
337 merged_cfg.policy.denied_repos = user_cfg.policy.denied_repos;
338 }
339 }
340
341 if !system_policy.signature_enforcement_unoverridable {
342 if project_cfg.policy.signature_enforcement.is_some() {
343 merged_cfg.policy.signature_enforcement =
344 project_cfg.policy.signature_enforcement;
345 } else if user_cfg.policy.signature_enforcement.is_some() {
346 merged_cfg.policy.signature_enforcement =
347 user_cfg.policy.signature_enforcement;
348 }
349 }
350
351 if let Some(url) = merged_cfg.registry.take()
352 && merged_cfg.default_registry.is_none()
353 {
354 merged_cfg.default_registry = Some(Registry {
355 handle: String::new(),
356 url,
357 advisory_prefix: None,
358 authorities: None
359 });
360 }
361
362 if merged_cfg.default_registry.is_none() {
363 merged_cfg.default_registry = Some(Registry {
364 handle: "zoidberg".to_string(),
365 url: get_default_registry(),
366 advisory_prefix: Some("ZSA".to_string()),
367 authorities: Some(get_builtin_authorities())
368 });
369 } else if let Some(ref mut reg) = merged_cfg.default_registry
370 && reg.url == get_default_registry()
371 && reg.authorities.as_ref().is_none_or(Vec::is_empty)
372 {
373 let builtin = get_builtin_authorities();
374 if !builtin.is_empty() {
375 reg.authorities = Some(builtin);
376 }
377 }
378
379 if merged_cfg.repos.is_empty()
380 && let Some(reg) = &merged_cfg.default_registry
381 && !reg.handle.is_empty()
382 {
383 let db_root = get_db_root()?;
384 let repo_path = db_root.join(®.handle);
385 if repo_path.join("repo.yaml").exists()
386 && let Ok(repo_config) = read_repo_config(&repo_path)
387 {
388 merged_cfg.repos = repo_config
389 .repos
390 .into_iter()
391 .filter(|r| r.active)
392 .map(|r| r.name)
393 .collect();
394 }
395 }
396
397 if project_val.get("system_generations_limit").is_some()
398 && !system_policy.system_generations_limit_unoverridable
399 {
400 merged_cfg.system_generations_limit =
401 project_cfg.system_generations_limit;
402 } else if user_val.get("system_generations_limit").is_some()
403 && !system_policy.system_generations_limit_unoverridable
404 {
405 merged_cfg.system_generations_limit = user_cfg.system_generations_limit;
406 } else {
407 merged_cfg.system_generations_limit =
408 system_cfg.system_generations_limit;
409 }
410
411 Ok(merged_cfg)
412}
413
414pub fn write_user_config(config: &Config) -> Result<()> {
421 let config_path = get_user_config_path()?;
422 let parent_dir = config_path
423 .parent()
424 .ok_or_else(|| anyhow!("Invalid config path"))?;
425 fs::create_dir_all(parent_dir)?;
426 let content = serde_yaml::to_string(config)?;
427 fs::write(config_path, content)?;
428 Ok(())
429}
430
431pub fn verify_remote_file(
439 url: &str,
440 trusted_keys: &[String]
441) -> Result<Vec<u8>> {
442 let client = crate::utils::get_http_client()?;
443
444 let response = client.get(url).send()?;
445 if !response.status().is_success() {
446 return Err(anyhow!(
447 "Failed to fetch remote file '{url}': {}",
448 response.status()
449 ));
450 }
451 let data = response.bytes()?;
452
453 let sig_url = format!("{url}.sig");
454 let sig_response = client.get(&sig_url).send()?;
455 if !sig_response.status().is_success() {
456 return Err(anyhow!(
457 "Failed to fetch signature for '{url}': {}",
458 sig_response.status()
459 ));
460 }
461 let sig = sig_response.bytes()?;
462
463 let trusted_certs =
464 crate::pgp::get_certs_by_name_or_fingerprint(trusted_keys)?;
465
466 let temp_dir = tempfile::Builder::new().prefix("zoi-verify-").tempdir()?;
467 let data_path = temp_dir.path().join("data");
468 let sig_path = temp_dir.path().join("sig");
469
470 fs::write(&data_path, &data)?;
471 fs::write(&sig_path, &sig)?;
472
473 crate::pgp::verify_detached_signature_multi_key(
474 &data_path,
475 &sig_path,
476 trusted_certs
477 )?;
478
479 Ok(data.to_vec())
480}
481
482pub fn add_repo(repo_name: &str) -> Result<()> {
489 let mut config = read_config_from_path(&get_user_config_path()?)?;
490 let lower_repo_name = repo_name.to_lowercase();
491 if config.repos.contains(&lower_repo_name) {
492 return Err(anyhow!(
493 "Repository '{repo_name}' already exists in user config."
494 ));
495 }
496 config.repos.push(lower_repo_name);
497 write_user_config(&config)
498}
499
500pub fn remove_repo(repo_name: &str) -> Result<()> {
507 let mut config = read_config_from_path(&get_user_config_path()?)?;
508 let lower_repo_name = repo_name.to_lowercase();
509 if let Some(pos) = config.repos.iter().position(|r| r == &lower_repo_name) {
510 config.repos.remove(pos);
511 write_user_config(&config)
512 } else {
513 Err(anyhow!(
514 "Repository '{repo_name}' not found in user config."
515 ))
516 }
517}
518
519pub fn interactive_add_repo() -> Result<()> {
528 let config = read_config()?;
529 let all_repos = get_all_repos()?;
530
531 let available_repos: Vec<_> = all_repos
532 .into_iter()
533 .filter(|r| !config.repos.contains(&r.to_lowercase()))
534 .collect();
535
536 if available_repos.is_empty() {
537 println!("{}", "No new repositories available to add.".yellow());
538 return Ok(());
539 }
540
541 println!("{}", "Available repositories to add:".green());
542 for (i, repo) in available_repos.iter().enumerate() {
543 println!("[{}] {}", i + 1, repo);
544 }
545
546 print!(
547 "\n{}",
548 "Select a repository to add (or 'q' to quit): ".yellow()
549 );
550 io::stdout().flush()?;
551
552 let mut input = String::new();
553 io::stdin().read_line(&mut input)?;
554 let input = input.trim();
555
556 if input == "q" {
557 println!("Aborted.");
558 return Ok(());
559 }
560
561 let choice: usize = match input.parse() {
562 Ok(num) => num,
563 Err(_) => return Err(anyhow!("Invalid input."))
564 };
565
566 if choice > 0 && choice <= available_repos.len() {
567 let repo_to_add = available_repos
568 .get(choice - 1)
569 .ok_or_else(|| anyhow!("Invalid choice"))?;
570 add_repo(repo_to_add)?;
571 println!("Repository '{}' added successfully.", repo_to_add.green());
572 } else {
573 return Err(anyhow!("Invalid selection."));
574 }
575
576 Ok(())
577}
578
579pub fn get_all_repos() -> Result<Vec<String>> {
585 let db_root = get_db_root()?;
586 let config = read_config()?;
587
588 if let Some(default_reg) = config.default_registry
589 && !default_reg.handle.is_empty()
590 {
591 let default_reg_path = db_root.join(default_reg.handle);
592 if default_reg_path.join("repo.yaml").exists() {
593 let repo_config = read_repo_config(&default_reg_path)?;
594 return Ok(repo_config.repos.into_iter().map(|r| r.name).collect());
595 }
596 }
597
598 Ok(Vec::new())
599}
600
601pub fn clone_git_repo(url: &str) -> Result<()> {
610 let git_root = get_git_root()?;
611 fs::create_dir_all(&git_root)?;
612 let repo_name = url
613 .trim_end_matches('/')
614 .split('/')
615 .next_back()
616 .unwrap_or("repo")
617 .trim_end_matches(".git");
618 let target = git_root.join(repo_name);
619 if target.exists() {
620 return Err(anyhow!(
621 "Git repo '{repo_name}' already exists at {}",
622 target.display()
623 ));
624 }
625 println!("Cloning '{}' into {}...", url.cyan(), target.display());
626 let status = std::process::Command::new("git")
627 .arg("clone")
628 .arg("--depth=1")
629 .arg(url)
630 .arg(&target)
631 .status()?;
632 if !status.success() {
633 return Err(anyhow!("git clone failed"));
634 }
635
636 let mut config = read_config_from_path(&get_user_config_path()?)?;
637 if !config.git_repos.iter().any(|repo_url| repo_url == url) {
638 config.git_repos.push(url.to_string());
639 write_user_config(&config)?;
640 }
641
642 println!(
643 "Cloned git repo as '{}' (use with '#git@{}/<pkg>')",
644 repo_name.green(),
645 repo_name
646 );
647 Ok(())
648}
649
650pub fn list_git_repos() -> Result<Vec<String>> {
656 let git_root = get_git_root()?;
657 if !git_root.exists() {
658 return Ok(Vec::new());
659 }
660
661 let mut repos = Vec::new();
662 for entry in fs::read_dir(git_root)? {
663 let entry = entry?;
664 if entry.path().is_dir() {
665 repos.push(entry.file_name().to_string_lossy().into_owned());
666 }
667 }
668 repos.sort();
669 Ok(repos)
670}
671
672pub fn remove_git_repo(repo_name: &str) -> Result<()> {
678 let git_root = get_git_root()?;
679 let target = git_root.join(repo_name);
680 if !target.exists() {
681 return Err(anyhow!("Git repository '{repo_name}' not found."));
682 }
683
684 let mut config = read_config_from_path(&get_user_config_path()?)?;
685 let mut removed = false;
686 config.git_repos.retain(|url| {
687 let name_from_url = url
688 .trim_end_matches('/')
689 .split('/')
690 .next_back()
691 .unwrap_or_default()
692 .trim_end_matches(".git");
693 if name_from_url == repo_name {
694 removed = true;
695 false
696 } else {
697 true
698 }
699 });
700
701 if removed {
702 write_user_config(&config)?;
703 }
704
705 fs::remove_dir_all(&target)?;
706 println!(
707 "Removed git repository '{}' from {}",
708 repo_name.green(),
709 target.display()
710 );
711 Ok(())
712}
713
714pub fn add_cache_mirror(url: &str) -> Result<()> {
721 let mut config = read_config_from_path(&get_user_config_path()?)?;
722 if config.cache_mirrors.iter().any(|existing| existing == url) {
723 return Err(anyhow!("Cache mirror '{url}' already exists."));
724 }
725 config.cache_mirrors.push(url.to_string());
726 write_user_config(&config)
727}
728
729pub fn remove_cache_mirror(url: &str) -> Result<()> {
736 let mut config = read_config_from_path(&get_user_config_path()?)?;
737 if let Some(pos) = config
738 .cache_mirrors
739 .iter()
740 .position(|existing| existing == url)
741 {
742 config.cache_mirrors.remove(pos);
743 write_user_config(&config)
744 } else {
745 Err(anyhow!("Cache mirror '{url}' not found."))
746 }
747}
748
749pub fn set_default_registry(url: &str) -> Result<()> {
755 let mut config = read_config_from_path(&get_user_config_path()?)?;
756 config.default_registry = Some(Registry {
757 handle: String::new(),
758 url: url.to_string(),
759 advisory_prefix: None,
760 authorities: None
761 });
762 write_user_config(&config)
763}
764
765pub fn set_user_default_registry(
771 default_registry: Option<Registry>
772) -> Result<()> {
773 let mut config = read_config_from_path(&get_user_config_path()?)?;
774 config.default_registry = default_registry;
775 write_user_config(&config)
776}
777
778pub fn add_added_registry(url: &str) -> Result<()> {
786 let mut config = read_config_from_path(&get_user_config_path()?)?;
787 if config.added_registries.iter().any(|r| r.url == url) {
788 return Err(anyhow!("Registry with URL '{url}' already exists."));
789 }
790 config.added_registries.push(Registry {
791 handle: String::new(),
792 url: url.to_string(),
793 advisory_prefix: None,
794 authorities: None
795 });
796 write_user_config(&config)
797}
798
799pub fn remove_added_registry(handle_or_url: &str) -> Result<()> {
806 let mut config = read_config_from_path(&get_user_config_path()?)?;
807 if let Some(pos) = config
808 .added_registries
809 .iter()
810 .position(|r| r.handle == handle_or_url || r.url == handle_or_url)
811 {
812 let removed_registry = config.added_registries.remove(pos);
813 if !removed_registry.handle.is_empty() {
814 let db_root = get_db_root()?;
815 let repo_path = db_root.join(removed_registry.handle);
816 if repo_path.exists() {
817 fs::remove_dir_all(repo_path)?;
818 }
819 }
820 write_user_config(&config)
821 } else {
822 Err(anyhow!(
823 "Added registry with handle or URL '{handle_or_url}' not found."
824 ))
825 }
826}
827
828pub fn read_repo_config(db_path: &Path) -> Result<RepoConfig> {
834 let config_path = db_path.join("repo.yaml");
835 if !config_path.exists() {
836 return Err(anyhow!(
837 "repo.yaml not found in the root of the package database."
838 ));
839 }
840 let content = fs::read_to_string(config_path)?;
841 let config: RepoConfig = serde_yaml::from_str(&content)?;
842 Ok(config)
843}
844
845pub fn read_user_config() -> Result<Config> {
851 read_config_from_path(&get_user_config_path()?)
852}
853
854pub fn update_global_versions<S: ::std::hash::BuildHasher>(
860 versions: HashMap<String, String, S>
861) -> Result<()> {
862 let mut config = read_user_config()?;
863 config.versions.extend(versions);
864 write_user_config(&config)
865}
866
867pub fn sync_remote_policy() -> Result<()> {
882 let config = read_config()?;
883 let Some(remote_cfg) = &config.remote_policy else {
884 return Ok(());
885 };
886
887 println!("{} Syncing remote security policy...", "::".bold().blue());
888
889 if crate::offline::is_offline() {
890 return Err(anyhow!("Cannot sync remote policy in offline mode."));
891 }
892
893 let client = crate::utils::get_http_client()?;
894
895 let policy_content = client.get(&remote_cfg.url).send()?.text()?;
896 let sig_content = client.get(&remote_cfg.signature_url).send()?.bytes()?;
897
898 let trusted_certs =
899 crate::pgp::get_certs_by_name_or_fingerprint(&remote_cfg.trusted_keys)?;
900
901 let temp_dir = tempfile::Builder::new().prefix("zoi-policy-").tempdir()?;
902 let temp_policy_path = temp_dir.path().join("policy.yaml");
903 let temp_sig_path = temp_dir.path().join("policy.yaml.sig");
904
905 fs::write(&temp_policy_path, &policy_content)?;
906 fs::write(&temp_sig_path, &sig_content)?;
907
908 crate::pgp::verify_detached_signature_multi_key(
909 &temp_policy_path,
910 &temp_sig_path,
911 trusted_certs
912 )?;
913
914 let cache_path = get_remote_policy_cache_path();
915 if let Some(parent) = cache_path.parent() {
916 fs::create_dir_all(parent)?;
917 }
918 fs::write(cache_path, policy_content)?;
919
920 println!(
921 "{}",
922 "Remote policy verified and cached successfully.".green()
923 );
924 Ok(())
925}
926
927fn merge_policies(
933 base: &mut crate::types::Policy,
934 remote: &crate::types::Policy
935) {
936 if remote.repos_unoverridable {
937 base.repos_unoverridable = true;
938 }
939 if remote.telemetry_enabled_unoverridable {
940 base.telemetry_enabled_unoverridable = true;
941 }
942 if remote.audit_log_enabled_unoverridable {
943 base.audit_log_enabled_unoverridable = true;
944 }
945 if remote.rollback_enabled_unoverridable {
946 base.rollback_enabled_unoverridable = true;
947 }
948 if remote.default_registry_unoverridable {
949 base.default_registry_unoverridable = true;
950 }
951 if remote.added_registries_unoverridable {
952 base.added_registries_unoverridable = true;
953 }
954 if remote.git_repos_unoverridable {
955 base.git_repos_unoverridable = true;
956 }
957 if remote.allow_deny_lists_unoverridable {
958 base.allow_deny_lists_unoverridable = true;
959 }
960 if remote.signature_enforcement_unoverridable {
961 base.signature_enforcement_unoverridable = true;
962 }
963 if remote.protect_db_unoverridable {
964 base.protect_db_unoverridable = true;
965 }
966 if remote.max_resolution_depth_unoverridable {
967 base.max_resolution_depth_unoverridable = true;
968 }
969 if remote.offline_mode_unoverridable {
970 base.offline_mode_unoverridable = true;
971 }
972 if remote.pkg_dirs_unoverridable {
973 base.pkg_dirs_unoverridable = true;
974 }
975 if remote.cache_mirrors_unoverridable {
976 base.cache_mirrors_unoverridable = true;
977 }
978 if remote.jobs_unoverridable {
979 base.jobs_unoverridable = true;
980 }
981 if remote.advisory_enforcement_unoverridable {
982 base.advisory_enforcement_unoverridable = true;
983 }
984
985 if let Some(allowed) = &remote.allowed_licenses {
986 base.allowed_licenses
987 .get_or_insert_with(Vec::new)
988 .extend(allowed.clone());
989 }
990 if let Some(denied) = &remote.denied_licenses {
991 base.denied_licenses
992 .get_or_insert_with(Vec::new)
993 .extend(denied.clone());
994 }
995 if let Some(allowed) = &remote.allowed_packages {
996 base.allowed_packages
997 .get_or_insert_with(Vec::new)
998 .extend(allowed.clone());
999 }
1000 if let Some(denied) = &remote.denied_packages {
1001 base.denied_packages
1002 .get_or_insert_with(Vec::new)
1003 .extend(denied.clone());
1004 }
1005 if let Some(allowed) = &remote.allowed_repos {
1006 base.allowed_repos
1007 .get_or_insert_with(Vec::new)
1008 .extend(allowed.clone());
1009 }
1010 if let Some(denied) = &remote.denied_repos {
1011 base.denied_repos
1012 .get_or_insert_with(Vec::new)
1013 .extend(denied.clone());
1014 }
1015
1016 if let Some(remote_sig) = &remote.signature_enforcement {
1017 if let Some(ref mut base_sig) = base.signature_enforcement {
1018 if remote_sig.enable {
1019 base_sig.enable = true;
1020 }
1021 base_sig
1022 .trusted_keys
1023 .extend(remote_sig.trusted_keys.clone());
1024 base_sig.trusted_keys.sort();
1025 base_sig.trusted_keys.dedup();
1026 } else {
1027 base.signature_enforcement = Some(remote_sig.clone());
1028 }
1029 }
1030}