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_path = get_system_config_path();
97 let system_val = read_yaml_value(&system_path)?;
98 let user_val = read_yaml_value(&get_user_config_path()?)?;
99 let project_val = read_yaml_value(&get_project_config_path()?)?;
100
101 let mut system_cfg: Config =
102 serde_yaml::from_value(system_val.clone()).unwrap_or_default();
103 let user_cfg: Config =
104 serde_yaml::from_value(user_val.clone()).unwrap_or_default();
105 let project_cfg: Config =
106 serde_yaml::from_value(project_val.clone()).unwrap_or_default();
107
108 let cache_path = get_remote_policy_cache_path();
109 if cache_path.exists()
110 && let Ok(cache_content) = fs::read_to_string(&cache_path)
111 && let Ok(remote_policy) =
112 serde_yaml::from_str::<crate::types::Policy>(&cache_content)
113 {
114 merge_policies(&mut system_cfg.policy, &remote_policy);
115 }
116
117 let system_policy = system_cfg.policy.clone();
118 let mut merged_cfg = Config {
119 policy: system_policy.clone(),
120 ..Default::default()
121 };
122
123 merged_cfg.repos = system_cfg.repos;
124 if !system_policy.repos_unoverridable {
125 merged_cfg.repos.extend(user_cfg.repos);
126 merged_cfg.repos.extend(project_cfg.repos);
127 }
128 merged_cfg.repos.sort();
129 merged_cfg.repos.dedup();
130
131 merged_cfg.added_registries = system_cfg.added_registries;
132 if !system_policy.added_registries_unoverridable {
133 merged_cfg
134 .added_registries
135 .extend(user_cfg.added_registries);
136 merged_cfg
137 .added_registries
138 .extend(project_cfg.added_registries);
139 }
140 let mut seen_registries = HashSet::new();
141 merged_cfg
142 .added_registries
143 .retain(|r| seen_registries.insert(r.url.clone()));
144
145 merged_cfg.git_repos = system_cfg.git_repos;
146 if !system_policy.git_repos_unoverridable {
147 merged_cfg.git_repos.extend(user_cfg.git_repos);
148 merged_cfg.git_repos.extend(project_cfg.git_repos);
149 }
150 merged_cfg.git_repos.sort();
151 merged_cfg.git_repos.dedup();
152
153 merged_cfg.package_managers = project_cfg
154 .package_managers
155 .or(user_cfg.package_managers)
156 .or(system_cfg.package_managers);
157 merged_cfg.native_package_manager = project_cfg
158 .native_package_manager
159 .or(user_cfg.native_package_manager)
160 .or(system_cfg.native_package_manager);
161 merged_cfg.registry = project_cfg
162 .registry
163 .or(user_cfg.registry)
164 .or(system_cfg.registry);
165
166 merged_cfg.remote_policy = system_cfg.remote_policy;
167
168 if project_val.get("telemetry_enabled").is_some()
169 && !system_policy.telemetry_enabled_unoverridable
170 {
171 merged_cfg.telemetry_enabled = project_cfg.telemetry_enabled;
172 } else if user_val.get("telemetry_enabled").is_some()
173 && !system_policy.telemetry_enabled_unoverridable
174 {
175 merged_cfg.telemetry_enabled = user_cfg.telemetry_enabled;
176 } else {
177 merged_cfg.telemetry_enabled = system_cfg.telemetry_enabled;
178 }
179
180 if project_val.get("audit_log_enabled").is_some()
181 && !system_policy.audit_log_enabled_unoverridable
182 {
183 merged_cfg.audit_log_enabled = project_cfg.audit_log_enabled;
184 } else if user_val.get("audit_log_enabled").is_some()
185 && !system_policy.audit_log_enabled_unoverridable
186 {
187 merged_cfg.audit_log_enabled = user_cfg.audit_log_enabled;
188 } else {
189 merged_cfg.audit_log_enabled = system_cfg.audit_log_enabled;
190 }
191
192 if project_val.get("rollback_enabled").is_some()
193 && !system_policy.rollback_enabled_unoverridable
194 {
195 merged_cfg.rollback_enabled = project_cfg.rollback_enabled;
196 } else if user_val.get("rollback_enabled").is_some()
197 && !system_policy.rollback_enabled_unoverridable
198 {
199 merged_cfg.rollback_enabled = user_cfg.rollback_enabled;
200 } else {
201 merged_cfg.rollback_enabled = system_cfg.rollback_enabled;
202 }
203
204 if project_val.get("default_registry").is_some()
205 && !system_policy.default_registry_unoverridable
206 {
207 merged_cfg.default_registry = project_cfg.default_registry;
208 } else if user_val.get("default_registry").is_some()
209 && !system_policy.default_registry_unoverridable
210 {
211 merged_cfg.default_registry = user_cfg.default_registry;
212 } else {
213 merged_cfg.default_registry = system_cfg.default_registry;
214 }
215
216 if project_val.get("jobs").is_some() && !system_policy.jobs_unoverridable {
217 merged_cfg.jobs = project_cfg.jobs;
218 } else if user_val.get("jobs").is_some()
219 && !system_policy.jobs_unoverridable
220 {
221 merged_cfg.jobs = user_cfg.jobs;
222 } else {
223 merged_cfg.jobs = system_cfg.jobs;
224 }
225
226 if project_val.get("protect_db").is_some()
227 && !system_policy.protect_db_unoverridable
228 {
229 merged_cfg.protect_db = project_cfg.protect_db;
230 } else if user_val.get("protect_db").is_some()
231 && !system_policy.protect_db_unoverridable
232 {
233 merged_cfg.protect_db = user_cfg.protect_db;
234 } else {
235 merged_cfg.protect_db = system_cfg.protect_db;
236 }
237
238 if project_val.get("max_resolution_depth").is_some()
239 && !system_policy.max_resolution_depth_unoverridable
240 {
241 merged_cfg.max_resolution_depth = project_cfg.max_resolution_depth;
242 } else if user_val.get("max_resolution_depth").is_some()
243 && !system_policy.max_resolution_depth_unoverridable
244 {
245 merged_cfg.max_resolution_depth = user_cfg.max_resolution_depth;
246 } else {
247 merged_cfg.max_resolution_depth = system_cfg.max_resolution_depth;
248 }
249
250 if project_val.get("offline_mode").is_some()
251 && !system_policy.offline_mode_unoverridable
252 {
253 merged_cfg.offline_mode = project_cfg.offline_mode;
254 } else if user_val.get("offline_mode").is_some()
255 && !system_policy.offline_mode_unoverridable
256 {
257 merged_cfg.offline_mode = user_cfg.offline_mode;
258 } else {
259 merged_cfg.offline_mode = system_cfg.offline_mode;
260 }
261
262 if project_val
263 .get("policy")
264 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
265 .is_some()
266 && !system_policy.advisory_enforcement_unoverridable
267 {
268 merged_cfg.policy.advisory_enforcement_unoverridable =
269 project_cfg.policy.advisory_enforcement_unoverridable;
270 } else if user_val
271 .get("policy")
272 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
273 .is_some()
274 && !system_policy.advisory_enforcement_unoverridable
275 {
276 merged_cfg.policy.advisory_enforcement_unoverridable =
277 user_cfg.policy.advisory_enforcement_unoverridable;
278 } else {
279 merged_cfg.policy.advisory_enforcement_unoverridable =
280 system_cfg.policy.advisory_enforcement_unoverridable;
281 }
282
283 merged_cfg.versions = system_cfg.versions;
284 merged_cfg.versions.extend(user_cfg.versions);
285 merged_cfg.versions.extend(project_cfg.versions);
286
287 merged_cfg.pkg_dirs = system_cfg.pkg_dirs;
288 if !system_policy.pkg_dirs_unoverridable {
289 merged_cfg.pkg_dirs.extend(user_cfg.pkg_dirs);
290 merged_cfg.pkg_dirs.extend(project_cfg.pkg_dirs);
291 }
292 merged_cfg.pkg_dirs.sort();
293 merged_cfg.pkg_dirs.dedup();
294
295 merged_cfg.cache_mirrors = system_cfg.cache_mirrors;
296 if !system_policy.cache_mirrors_unoverridable {
297 merged_cfg.cache_mirrors.extend(user_cfg.cache_mirrors);
298 merged_cfg.cache_mirrors.extend(project_cfg.cache_mirrors);
299 }
300 merged_cfg.cache_mirrors.sort();
301 merged_cfg.cache_mirrors.dedup();
302
303 if !system_policy.allow_deny_lists_unoverridable {
304 if project_cfg.policy.allowed_licenses.is_some() {
305 merged_cfg.policy.allowed_licenses =
306 project_cfg.policy.allowed_licenses;
307 } else if user_cfg.policy.allowed_licenses.is_some() {
308 merged_cfg.policy.allowed_licenses =
309 user_cfg.policy.allowed_licenses;
310 }
311 if project_cfg.policy.denied_licenses.is_some() {
312 merged_cfg.policy.denied_licenses =
313 project_cfg.policy.denied_licenses;
314 } else if user_cfg.policy.denied_licenses.is_some() {
315 merged_cfg.policy.denied_licenses = user_cfg.policy.denied_licenses;
316 }
317 if project_cfg.policy.allowed_packages.is_some() {
318 merged_cfg.policy.allowed_packages =
319 project_cfg.policy.allowed_packages;
320 } else if user_cfg.policy.allowed_packages.is_some() {
321 merged_cfg.policy.allowed_packages =
322 user_cfg.policy.allowed_packages;
323 }
324 if project_cfg.policy.denied_packages.is_some() {
325 merged_cfg.policy.denied_packages =
326 project_cfg.policy.denied_packages;
327 } else if user_cfg.policy.denied_packages.is_some() {
328 merged_cfg.policy.denied_packages = user_cfg.policy.denied_packages;
329 }
330 if project_cfg.policy.allowed_repos.is_some() {
331 merged_cfg.policy.allowed_repos = project_cfg.policy.allowed_repos;
332 } else if user_cfg.policy.allowed_repos.is_some() {
333 merged_cfg.policy.allowed_repos = user_cfg.policy.allowed_repos;
334 }
335 if project_cfg.policy.denied_repos.is_some() {
336 merged_cfg.policy.denied_repos = project_cfg.policy.denied_repos;
337 } else if user_cfg.policy.denied_repos.is_some() {
338 merged_cfg.policy.denied_repos = user_cfg.policy.denied_repos;
339 }
340 }
341
342 if !system_policy.signature_enforcement_unoverridable {
343 if project_cfg.policy.signature_enforcement.is_some() {
344 merged_cfg.policy.signature_enforcement =
345 project_cfg.policy.signature_enforcement;
346 } else if user_cfg.policy.signature_enforcement.is_some() {
347 merged_cfg.policy.signature_enforcement =
348 user_cfg.policy.signature_enforcement;
349 }
350 }
351
352 if let Some(url) = merged_cfg.registry.take()
353 && merged_cfg.default_registry.is_none()
354 {
355 merged_cfg.default_registry = Some(Registry {
356 handle: String::new(),
357 url,
358 name: None,
359 description: None,
360 advisory_prefix: None,
361 authorities: None
362 });
363 }
364
365 if merged_cfg.default_registry.is_none() {
366 let set_registry = crate::builtin::registry::get_set();
369 let (handle, url, advisory_prefix, authorities) = match set_registry {
370 Ok(Some(reg)) => {
371 let builtin = get_builtin_authorities();
372 (
373 reg.handle,
374 reg.git,
375 Some("ZSA".to_string()),
376 if builtin.is_empty() {
377 None
378 } else {
379 Some(builtin)
380 }
381 )
382 }
383 Ok(None) | Err(_) => (
386 "zoidberg".to_string(),
387 get_default_registry(),
388 Some("ZSA".to_string()),
389 {
390 let b = get_builtin_authorities();
391 if b.is_empty() { None } else { Some(b) }
392 }
393 )
394 };
395 merged_cfg.default_registry = Some(Registry {
396 handle,
397 url,
398 name: None,
399 description: None,
400 advisory_prefix,
401 authorities
402 });
403 } else if let Some(ref mut reg) = merged_cfg.default_registry
404 && reg.url == get_default_registry()
405 && reg.authorities.as_ref().is_none_or(Vec::is_empty)
406 {
407 let builtin = get_builtin_authorities();
408 if !builtin.is_empty() {
409 reg.authorities = Some(builtin);
410 }
411 }
412
413 if merged_cfg.repos.is_empty()
414 && let Some(reg) = &merged_cfg.default_registry
415 && !reg.handle.is_empty()
416 {
417 let db_root = get_db_root()?;
418 let repo_path = db_root.join(®.handle);
419 if repo_path.join("repo.yaml").exists()
420 && let Ok(repo_config) = read_repo_config(&repo_path)
421 {
422 merged_cfg.repos = repo_config
423 .repos
424 .into_iter()
425 .filter(|r| r.active)
426 .map(|r| r.name)
427 .collect();
428 }
429 }
430
431 if project_val.get("system_generations_limit").is_some()
432 && !system_policy.system_generations_limit_unoverridable
433 {
434 merged_cfg.system_generations_limit =
435 project_cfg.system_generations_limit;
436 } else if user_val.get("system_generations_limit").is_some()
437 && !system_policy.system_generations_limit_unoverridable
438 {
439 merged_cfg.system_generations_limit = user_cfg.system_generations_limit;
440 } else {
441 merged_cfg.system_generations_limit =
442 system_cfg.system_generations_limit;
443 }
444
445 if project_val.get("max_delta_steps").is_some()
446 && !system_policy.max_delta_steps_unoverridable
447 {
448 merged_cfg.max_delta_steps = project_cfg.max_delta_steps;
449 } else if user_val.get("max_delta_steps").is_some()
450 && !system_policy.max_delta_steps_unoverridable
451 {
452 merged_cfg.max_delta_steps = user_cfg.max_delta_steps;
453 } else {
454 merged_cfg.max_delta_steps = system_cfg.max_delta_steps;
455 }
456
457 Ok(merged_cfg)
458}
459
460pub fn write_user_config(config: &Config) -> Result<()> {
467 let config_path = get_user_config_path()?;
468 let parent_dir = config_path
469 .parent()
470 .ok_or_else(|| anyhow!("Invalid config path"))?;
471 fs::create_dir_all(parent_dir)?;
472 let content = serde_yaml::to_string(config)?;
473 fs::write(config_path, content)?;
474 Ok(())
475}
476
477pub fn verify_remote_file(
485 url: &str,
486 trusted_keys: &[String]
487) -> Result<Vec<u8>> {
488 let client = crate::utils::get_http_client()?;
489
490 let response = client.get(url).send()?;
491 if !response.status().is_success() {
492 return Err(anyhow!(
493 "Failed to fetch remote file '{url}': {}",
494 response.status()
495 ));
496 }
497 let data = response.bytes()?;
498
499 let sig_url = format!("{url}.sig");
500 let sig_response = client.get(&sig_url).send()?;
501 if !sig_response.status().is_success() {
502 return Err(anyhow!(
503 "Failed to fetch signature for '{url}': {}",
504 sig_response.status()
505 ));
506 }
507 let sig = sig_response.bytes()?;
508
509 let trusted_certs =
510 crate::pgp::get_certs_by_name_or_fingerprint(trusted_keys)?;
511
512 let temp_dir = tempfile::Builder::new().prefix("zoi-verify-").tempdir()?;
513 let data_path = temp_dir.path().join("data");
514 let sig_path = temp_dir.path().join("sig");
515
516 fs::write(&data_path, &data)?;
517 fs::write(&sig_path, &sig)?;
518
519 crate::pgp::verify_detached_signature_multi_key(
520 &data_path,
521 &sig_path,
522 trusted_certs
523 )?;
524
525 Ok(data.to_vec())
526}
527
528pub fn add_repo(repo_name: &str) -> Result<()> {
535 let mut config = read_config_from_path(&get_user_config_path()?)?;
536 let lower_repo_name = repo_name.to_lowercase();
537 if config.repos.contains(&lower_repo_name) {
538 return Err(anyhow!(
539 "Repository '{repo_name}' already exists in user config."
540 ));
541 }
542 config.repos.push(lower_repo_name);
543 write_user_config(&config)
544}
545
546pub fn remove_repo(repo_name: &str) -> Result<()> {
553 let mut config = read_config_from_path(&get_user_config_path()?)?;
554 let lower_repo_name = repo_name.to_lowercase();
555 if let Some(pos) = config.repos.iter().position(|r| r == &lower_repo_name) {
556 config.repos.remove(pos);
557 write_user_config(&config)
558 } else {
559 Err(anyhow!(
560 "Repository '{repo_name}' not found in user config."
561 ))
562 }
563}
564
565pub fn interactive_add_repo() -> Result<()> {
574 let config = read_config()?;
575 let all_repos = get_all_repos()?;
576
577 let available_repos: Vec<_> = all_repos
578 .into_iter()
579 .filter(|r| !config.repos.contains(&r.to_lowercase()))
580 .collect();
581
582 if available_repos.is_empty() {
583 println!("{}", "No new repositories available to add.".yellow());
584 return Ok(());
585 }
586
587 println!("{}", "Available repositories to add:".green());
588 for (i, repo) in available_repos.iter().enumerate() {
589 println!("[{}] {}", i + 1, repo);
590 }
591
592 print!(
593 "\n{}",
594 "Select a repository to add (or 'q' to quit): ".yellow()
595 );
596 io::stdout().flush()?;
597
598 let mut input = String::new();
599 io::stdin().read_line(&mut input)?;
600 let input = input.trim();
601
602 if input == "q" {
603 println!("Aborted.");
604 return Ok(());
605 }
606
607 let choice: usize = match input.parse() {
608 Ok(num) => num,
609 Err(_) => return Err(anyhow!("Invalid input."))
610 };
611
612 if choice > 0 && choice <= available_repos.len() {
613 let repo_to_add = available_repos
614 .get(choice - 1)
615 .ok_or_else(|| anyhow!("Invalid choice"))?;
616 add_repo(repo_to_add)?;
617 println!("Repository '{}' added successfully.", repo_to_add.green());
618 } else {
619 return Err(anyhow!("Invalid selection."));
620 }
621
622 Ok(())
623}
624
625pub fn get_all_repos() -> Result<Vec<String>> {
631 let db_root = get_db_root()?;
632 let config = read_config()?;
633
634 if let Some(default_reg) = config.default_registry
635 && !default_reg.handle.is_empty()
636 {
637 let default_reg_path = db_root.join(default_reg.handle);
638 if default_reg_path.join("repo.yaml").exists() {
639 let repo_config = read_repo_config(&default_reg_path)?;
640 return Ok(repo_config.repos.into_iter().map(|r| r.name).collect());
641 }
642 }
643
644 Ok(Vec::new())
645}
646
647pub fn clone_git_repo(url: &str) -> Result<()> {
656 let git_root = get_git_root()?;
657 fs::create_dir_all(&git_root)?;
658 let repo_name = url
659 .trim_end_matches('/')
660 .split('/')
661 .next_back()
662 .unwrap_or("repo")
663 .trim_end_matches(".git");
664 let target = git_root.join(repo_name);
665 if target.exists() {
666 return Err(anyhow!(
667 "Git repo '{repo_name}' already exists at {}",
668 target.display()
669 ));
670 }
671 println!("Cloning '{}' into {}...", url.cyan(), target.display());
672 let status = std::process::Command::new("git")
673 .arg("clone")
674 .arg("--depth=1")
675 .arg(url)
676 .arg(&target)
677 .status()?;
678 if !status.success() {
679 return Err(anyhow!("git clone failed"));
680 }
681
682 let mut config = read_config_from_path(&get_user_config_path()?)?;
683 if !config.git_repos.iter().any(|repo_url| repo_url == url) {
684 config.git_repos.push(url.to_string());
685 write_user_config(&config)?;
686 }
687
688 println!(
689 "Cloned git repo as '{}' (use with '#git@{}/<pkg>')",
690 repo_name.green(),
691 repo_name
692 );
693 Ok(())
694}
695
696pub fn list_git_repos() -> Result<Vec<String>> {
702 let git_root = get_git_root()?;
703 if !git_root.exists() {
704 return Ok(Vec::new());
705 }
706
707 let mut repos = Vec::new();
708 for entry in fs::read_dir(git_root)? {
709 let entry = entry?;
710 if entry.path().is_dir() {
711 repos.push(entry.file_name().to_string_lossy().into_owned());
712 }
713 }
714 repos.sort();
715 Ok(repos)
716}
717
718pub fn remove_git_repo(repo_name: &str) -> Result<()> {
724 let git_root = get_git_root()?;
725 let target = git_root.join(repo_name);
726 if !target.exists() {
727 return Err(anyhow!("Git repository '{repo_name}' not found."));
728 }
729
730 let mut config = read_config_from_path(&get_user_config_path()?)?;
731 let mut removed = false;
732 config.git_repos.retain(|url| {
733 let name_from_url = url
734 .trim_end_matches('/')
735 .split('/')
736 .next_back()
737 .unwrap_or_default()
738 .trim_end_matches(".git");
739 if name_from_url == repo_name {
740 removed = true;
741 false
742 } else {
743 true
744 }
745 });
746
747 if removed {
748 write_user_config(&config)?;
749 }
750
751 fs::remove_dir_all(&target)?;
752 println!(
753 "Removed git repository '{}' from {}",
754 repo_name.green(),
755 target.display()
756 );
757 Ok(())
758}
759
760pub fn add_cache_mirror(url: &str) -> Result<()> {
767 let mut config = read_config_from_path(&get_user_config_path()?)?;
768 if config.cache_mirrors.iter().any(|existing| existing == url) {
769 return Err(anyhow!("Cache mirror '{url}' already exists."));
770 }
771 config.cache_mirrors.push(url.to_string());
772 write_user_config(&config)
773}
774
775pub fn remove_cache_mirror(url: &str) -> Result<()> {
782 let mut config = read_config_from_path(&get_user_config_path()?)?;
783 if let Some(pos) = config
784 .cache_mirrors
785 .iter()
786 .position(|existing| existing == url)
787 {
788 config.cache_mirrors.remove(pos);
789 write_user_config(&config)
790 } else {
791 Err(anyhow!("Cache mirror '{url}' not found."))
792 }
793}
794
795fn authorities_for_builtin(
802 builtin: &crate::types::BuiltinRegistry
803) -> Option<Vec<String>> {
804 if !builtin.set {
805 return None;
806 }
807 let authorities = get_builtin_authorities();
808 if authorities.is_empty() {
809 None
810 } else {
811 Some(authorities)
812 }
813}
814
815pub fn set_default_registry(url_or_handle: &str) -> Result<()> {
827 let mut config = read_config_from_path(&get_user_config_path()?)?;
828
829 let registry =
830 if let Some(builtin) = crate::builtin::registry::get(url_or_handle) {
831 let authorities = authorities_for_builtin(&builtin);
832 Registry {
833 handle: builtin.handle.clone(),
834 url: builtin.git,
835 name: Some(builtin.name),
836 description: Some(builtin.description),
837 advisory_prefix: None,
838 authorities
839 }
840 } else {
841 Registry {
842 handle: String::new(),
843 url: url_or_handle.to_string(),
844 name: None,
845 description: None,
846 advisory_prefix: None,
847 authorities: None
848 }
849 };
850
851 config.default_registry = Some(registry);
852 write_user_config(&config)
853}
854
855pub fn set_user_default_registry(
861 default_registry: Option<Registry>
862) -> Result<()> {
863 let mut config = read_config_from_path(&get_user_config_path()?)?;
864 config.default_registry = default_registry;
865 write_user_config(&config)
866}
867
868pub fn add_added_registry(url_or_handle: &str) -> Result<()> {
880 let mut config = read_config_from_path(&get_user_config_path()?)?;
881
882 let (handle, url, name, description, authorities) =
883 if let Some(builtin) = crate::builtin::registry::get(url_or_handle) {
884 let authorities = authorities_for_builtin(&builtin);
885 (
886 Some(builtin.handle),
887 builtin.git,
888 Some(builtin.name),
889 Some(builtin.description),
890 authorities
891 )
892 } else {
893 (None, url_or_handle.to_string(), None, None, None)
894 };
895
896 if config.added_registries.iter().any(|r| r.url == url)
897 || config
898 .added_registries
899 .iter()
900 .any(|r| handle.as_deref().is_some_and(|h| r.handle == h))
901 {
902 return Err(anyhow!(
903 "Registry with URL or handle '{url_or_handle}' already exists."
904 ));
905 }
906
907 config.added_registries.push(Registry {
908 handle: handle.unwrap_or_default(),
909 url,
910 name,
911 description,
912 advisory_prefix: None,
913 authorities
914 });
915 write_user_config(&config)
916}
917
918pub fn remove_added_registry(handle_or_url: &str) -> Result<()> {
925 let mut config = read_config_from_path(&get_user_config_path()?)?;
926 if let Some(pos) = config
927 .added_registries
928 .iter()
929 .position(|r| r.handle == handle_or_url || r.url == handle_or_url)
930 {
931 let removed_registry = config.added_registries.remove(pos);
932 if !removed_registry.handle.is_empty() {
933 let db_root = get_db_root()?;
934 let repo_path = db_root.join(removed_registry.handle);
935 if repo_path.exists() {
936 fs::remove_dir_all(repo_path)?;
937 }
938 }
939 write_user_config(&config)
940 } else {
941 Err(anyhow!(
942 "Added registry with handle or URL '{handle_or_url}' not found."
943 ))
944 }
945}
946
947pub fn read_repo_config(db_path: &Path) -> Result<RepoConfig> {
953 let config_path = db_path.join("repo.yaml");
954 if !config_path.exists() {
955 return Err(anyhow!(
956 "repo.yaml not found in the root of the package database."
957 ));
958 }
959 let content = fs::read_to_string(config_path)?;
960 let config: RepoConfig = serde_yaml::from_str(&content)?;
961 Ok(config)
962}
963
964pub fn read_user_config() -> Result<Config> {
970 read_config_from_path(&get_user_config_path()?)
971}
972
973pub fn update_global_versions<S: ::std::hash::BuildHasher>(
979 versions: HashMap<String, String, S>
980) -> Result<()> {
981 let mut config = read_user_config()?;
982 config.versions.extend(versions);
983 write_user_config(&config)
984}
985
986pub fn sync_remote_policy() -> Result<()> {
1001 let config = read_config()?;
1002 let Some(remote_cfg) = &config.remote_policy else {
1003 return Ok(());
1004 };
1005
1006 println!("{} Syncing remote security policy...", "::".bold().blue());
1007
1008 if crate::offline::is_offline() {
1009 return Err(anyhow!("Cannot sync remote policy in offline mode."));
1010 }
1011
1012 let client = crate::utils::get_http_client()?;
1013
1014 let policy_content = client.get(&remote_cfg.url).send()?.text()?;
1015 let sig_content = client.get(&remote_cfg.signature_url).send()?.bytes()?;
1016
1017 let trusted_certs =
1018 crate::pgp::get_certs_by_name_or_fingerprint(&remote_cfg.trusted_keys)?;
1019
1020 let temp_dir = tempfile::Builder::new().prefix("zoi-policy-").tempdir()?;
1021 let temp_policy_path = temp_dir.path().join("policy.yaml");
1022 let temp_sig_path = temp_dir.path().join("policy.yaml.sig");
1023
1024 fs::write(&temp_policy_path, &policy_content)?;
1025 fs::write(&temp_sig_path, &sig_content)?;
1026
1027 crate::pgp::verify_detached_signature_multi_key(
1028 &temp_policy_path,
1029 &temp_sig_path,
1030 trusted_certs
1031 )?;
1032
1033 let cache_path = get_remote_policy_cache_path();
1034 if let Some(parent) = cache_path.parent() {
1035 fs::create_dir_all(parent)?;
1036 }
1037 fs::write(cache_path, policy_content)?;
1038
1039 println!(
1040 "{}",
1041 "Remote policy verified and cached successfully.".green()
1042 );
1043 Ok(())
1044}
1045
1046fn merge_policies(
1052 base: &mut crate::types::Policy,
1053 remote: &crate::types::Policy
1054) {
1055 if remote.repos_unoverridable {
1056 base.repos_unoverridable = true;
1057 }
1058 if remote.telemetry_enabled_unoverridable {
1059 base.telemetry_enabled_unoverridable = true;
1060 }
1061 if remote.audit_log_enabled_unoverridable {
1062 base.audit_log_enabled_unoverridable = true;
1063 }
1064 if remote.rollback_enabled_unoverridable {
1065 base.rollback_enabled_unoverridable = true;
1066 }
1067 if remote.default_registry_unoverridable {
1068 base.default_registry_unoverridable = true;
1069 }
1070 if remote.added_registries_unoverridable {
1071 base.added_registries_unoverridable = true;
1072 }
1073 if remote.git_repos_unoverridable {
1074 base.git_repos_unoverridable = true;
1075 }
1076 if remote.allow_deny_lists_unoverridable {
1077 base.allow_deny_lists_unoverridable = true;
1078 }
1079 if remote.signature_enforcement_unoverridable {
1080 base.signature_enforcement_unoverridable = true;
1081 }
1082 if remote.protect_db_unoverridable {
1083 base.protect_db_unoverridable = true;
1084 }
1085 if remote.max_resolution_depth_unoverridable {
1086 base.max_resolution_depth_unoverridable = true;
1087 }
1088 if remote.offline_mode_unoverridable {
1089 base.offline_mode_unoverridable = true;
1090 }
1091 if remote.pkg_dirs_unoverridable {
1092 base.pkg_dirs_unoverridable = true;
1093 }
1094 if remote.cache_mirrors_unoverridable {
1095 base.cache_mirrors_unoverridable = true;
1096 }
1097 if remote.jobs_unoverridable {
1098 base.jobs_unoverridable = true;
1099 }
1100 if remote.advisory_enforcement_unoverridable {
1101 base.advisory_enforcement_unoverridable = true;
1102 }
1103
1104 if let Some(allowed) = &remote.allowed_licenses {
1105 base.allowed_licenses
1106 .get_or_insert_with(Vec::new)
1107 .extend(allowed.clone());
1108 }
1109 if let Some(denied) = &remote.denied_licenses {
1110 base.denied_licenses
1111 .get_or_insert_with(Vec::new)
1112 .extend(denied.clone());
1113 }
1114 if let Some(allowed) = &remote.allowed_packages {
1115 base.allowed_packages
1116 .get_or_insert_with(Vec::new)
1117 .extend(allowed.clone());
1118 }
1119 if let Some(denied) = &remote.denied_packages {
1120 base.denied_packages
1121 .get_or_insert_with(Vec::new)
1122 .extend(denied.clone());
1123 }
1124 if let Some(allowed) = &remote.allowed_repos {
1125 base.allowed_repos
1126 .get_or_insert_with(Vec::new)
1127 .extend(allowed.clone());
1128 }
1129 if let Some(denied) = &remote.denied_repos {
1130 base.denied_repos
1131 .get_or_insert_with(Vec::new)
1132 .extend(denied.clone());
1133 }
1134
1135 if let Some(remote_sig) = &remote.signature_enforcement {
1136 if let Some(ref mut base_sig) = base.signature_enforcement {
1137 if remote_sig.enable {
1138 base_sig.enable = true;
1139 }
1140 base_sig
1141 .trusted_keys
1142 .extend(remote_sig.trusted_keys.clone());
1143 base_sig.trusted_keys.sort();
1144 base_sig.trusted_keys.dedup();
1145 } else {
1146 base.signature_enforcement = Some(remote_sig.clone());
1147 }
1148 }
1149}