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("kernel_retention").is_some()
446 && !system_policy.kernel_retention_unoverridable
447 {
448 merged_cfg.kernel_retention = project_cfg.kernel_retention;
449 } else if user_val.get("kernel_retention").is_some()
450 && !system_policy.kernel_retention_unoverridable
451 {
452 merged_cfg.kernel_retention = user_cfg.kernel_retention;
453 } else if system_val.get("kernel_retention").is_some() {
454 merged_cfg.kernel_retention = system_cfg.kernel_retention;
455 }
456
457 if project_val.get("max_delta_steps").is_some()
458 && !system_policy.max_delta_steps_unoverridable
459 {
460 merged_cfg.max_delta_steps = project_cfg.max_delta_steps;
461 } else if user_val.get("max_delta_steps").is_some()
462 && !system_policy.max_delta_steps_unoverridable
463 {
464 merged_cfg.max_delta_steps = user_cfg.max_delta_steps;
465 } else {
466 merged_cfg.max_delta_steps = system_cfg.max_delta_steps;
467 }
468
469 Ok(merged_cfg)
470}
471
472pub fn write_user_config(config: &Config) -> Result<()> {
479 let config_path = get_user_config_path()?;
480 let parent_dir = config_path
481 .parent()
482 .ok_or_else(|| anyhow!("Invalid config path"))?;
483 fs::create_dir_all(parent_dir)?;
484 let content = serde_yaml::to_string(config)?;
485 fs::write(config_path, content)?;
486 Ok(())
487}
488
489pub fn verify_remote_file(
497 url: &str,
498 trusted_keys: &[String]
499) -> Result<Vec<u8>> {
500 let client = crate::utils::get_http_client()?;
501
502 let response = client.get(url).send()?;
503 if !response.status().is_success() {
504 return Err(anyhow!(
505 "Failed to fetch remote file '{url}': {}",
506 response.status()
507 ));
508 }
509 let data = response.bytes()?;
510
511 let sig_url = format!("{url}.sig");
512 let sig_response = client.get(&sig_url).send()?;
513 if !sig_response.status().is_success() {
514 return Err(anyhow!(
515 "Failed to fetch signature for '{url}': {}",
516 sig_response.status()
517 ));
518 }
519 let sig = sig_response.bytes()?;
520
521 let trusted_certs =
522 crate::pgp::get_certs_by_name_or_fingerprint(trusted_keys)?;
523
524 let temp_dir = tempfile::Builder::new().prefix("zoi-verify-").tempdir()?;
525 let data_path = temp_dir.path().join("data");
526 let sig_path = temp_dir.path().join("sig");
527
528 fs::write(&data_path, &data)?;
529 fs::write(&sig_path, &sig)?;
530
531 crate::pgp::verify_detached_signature_multi_key(
532 &data_path,
533 &sig_path,
534 trusted_certs
535 )?;
536
537 Ok(data.to_vec())
538}
539
540pub fn add_repo(repo_name: &str) -> Result<()> {
547 let mut config = read_config_from_path(&get_user_config_path()?)?;
548 let lower_repo_name = repo_name.to_lowercase();
549 if config.repos.contains(&lower_repo_name) {
550 return Err(anyhow!(
551 "Repository '{repo_name}' already exists in user config."
552 ));
553 }
554 config.repos.push(lower_repo_name);
555 write_user_config(&config)
556}
557
558pub fn remove_repo(repo_name: &str) -> Result<()> {
565 let mut config = read_config_from_path(&get_user_config_path()?)?;
566 let lower_repo_name = repo_name.to_lowercase();
567 if let Some(pos) = config.repos.iter().position(|r| r == &lower_repo_name) {
568 config.repos.remove(pos);
569 write_user_config(&config)
570 } else {
571 Err(anyhow!(
572 "Repository '{repo_name}' not found in user config."
573 ))
574 }
575}
576
577pub fn interactive_add_repo() -> Result<()> {
586 let config = read_config()?;
587 let all_repos = get_all_repos()?;
588
589 let available_repos: Vec<_> = all_repos
590 .into_iter()
591 .filter(|r| !config.repos.contains(&r.to_lowercase()))
592 .collect();
593
594 if available_repos.is_empty() {
595 println!("{}", "No new repositories available to add.".yellow());
596 return Ok(());
597 }
598
599 println!("{}", "Available repositories to add:".green());
600 for (i, repo) in available_repos.iter().enumerate() {
601 println!("[{}] {}", i + 1, repo);
602 }
603
604 print!(
605 "\n{}",
606 "Select a repository to add (or 'q' to quit): ".yellow()
607 );
608 io::stdout().flush()?;
609
610 let mut input = String::new();
611 io::stdin().read_line(&mut input)?;
612 let input = input.trim();
613
614 if input == "q" {
615 println!("Aborted.");
616 return Ok(());
617 }
618
619 let choice: usize = match input.parse() {
620 Ok(num) => num,
621 Err(_) => return Err(anyhow!("Invalid input."))
622 };
623
624 if choice > 0 && choice <= available_repos.len() {
625 let repo_to_add = available_repos
626 .get(choice - 1)
627 .ok_or_else(|| anyhow!("Invalid choice"))?;
628 add_repo(repo_to_add)?;
629 println!("Repository '{}' added successfully.", repo_to_add.green());
630 } else {
631 return Err(anyhow!("Invalid selection."));
632 }
633
634 Ok(())
635}
636
637pub fn get_all_repos() -> Result<Vec<String>> {
643 let db_root = get_db_root()?;
644 let config = read_config()?;
645
646 if let Some(default_reg) = config.default_registry
647 && !default_reg.handle.is_empty()
648 {
649 let default_reg_path = db_root.join(default_reg.handle);
650 if default_reg_path.join("repo.yaml").exists() {
651 let repo_config = read_repo_config(&default_reg_path)?;
652 return Ok(repo_config.repos.into_iter().map(|r| r.name).collect());
653 }
654 }
655
656 Ok(Vec::new())
657}
658
659pub fn clone_git_repo(url: &str) -> Result<()> {
668 let git_root = get_git_root()?;
669 fs::create_dir_all(&git_root)?;
670 let repo_name = url
671 .trim_end_matches('/')
672 .split('/')
673 .next_back()
674 .unwrap_or("repo")
675 .trim_end_matches(".git");
676 let target = git_root.join(repo_name);
677 if target.exists() {
678 return Err(anyhow!(
679 "Git repo '{repo_name}' already exists at {}",
680 target.display()
681 ));
682 }
683 println!("Cloning '{}' into {}...", url.cyan(), target.display());
684 let status = std::process::Command::new("git")
685 .arg("clone")
686 .arg("--depth=1")
687 .arg(url)
688 .arg(&target)
689 .status()?;
690 if !status.success() {
691 return Err(anyhow!("git clone failed"));
692 }
693
694 let mut config = read_config_from_path(&get_user_config_path()?)?;
695 if !config.git_repos.iter().any(|repo_url| repo_url == url) {
696 config.git_repos.push(url.to_string());
697 write_user_config(&config)?;
698 }
699
700 println!(
701 "Cloned git repo as '{}' (use with '#git@{}/<pkg>')",
702 repo_name.green(),
703 repo_name
704 );
705 Ok(())
706}
707
708pub fn list_git_repos() -> Result<Vec<String>> {
714 let git_root = get_git_root()?;
715 if !git_root.exists() {
716 return Ok(Vec::new());
717 }
718
719 let mut repos = Vec::new();
720 for entry in fs::read_dir(git_root)? {
721 let entry = entry?;
722 if entry.path().is_dir() {
723 repos.push(entry.file_name().to_string_lossy().into_owned());
724 }
725 }
726 repos.sort();
727 Ok(repos)
728}
729
730pub fn remove_git_repo(repo_name: &str) -> Result<()> {
736 let git_root = get_git_root()?;
737 let target = git_root.join(repo_name);
738 if !target.exists() {
739 return Err(anyhow!("Git repository '{repo_name}' not found."));
740 }
741
742 let mut config = read_config_from_path(&get_user_config_path()?)?;
743 let mut removed = false;
744 config.git_repos.retain(|url| {
745 let name_from_url = url
746 .trim_end_matches('/')
747 .split('/')
748 .next_back()
749 .unwrap_or_default()
750 .trim_end_matches(".git");
751 if name_from_url == repo_name {
752 removed = true;
753 false
754 } else {
755 true
756 }
757 });
758
759 if removed {
760 write_user_config(&config)?;
761 }
762
763 fs::remove_dir_all(&target)?;
764 println!(
765 "Removed git repository '{}' from {}",
766 repo_name.green(),
767 target.display()
768 );
769 Ok(())
770}
771
772pub fn add_cache_mirror(url: &str) -> Result<()> {
779 let mut config = read_config_from_path(&get_user_config_path()?)?;
780 if config.cache_mirrors.iter().any(|existing| existing == url) {
781 return Err(anyhow!("Cache mirror '{url}' already exists."));
782 }
783 config.cache_mirrors.push(url.to_string());
784 write_user_config(&config)
785}
786
787pub fn remove_cache_mirror(url: &str) -> Result<()> {
794 let mut config = read_config_from_path(&get_user_config_path()?)?;
795 if let Some(pos) = config
796 .cache_mirrors
797 .iter()
798 .position(|existing| existing == url)
799 {
800 config.cache_mirrors.remove(pos);
801 write_user_config(&config)
802 } else {
803 Err(anyhow!("Cache mirror '{url}' not found."))
804 }
805}
806
807fn authorities_for_builtin(
814 builtin: &crate::types::BuiltinRegistry
815) -> Option<Vec<String>> {
816 if !builtin.set {
817 return None;
818 }
819 let authorities = get_builtin_authorities();
820 if authorities.is_empty() {
821 None
822 } else {
823 Some(authorities)
824 }
825}
826
827pub fn set_default_registry(url_or_handle: &str) -> Result<()> {
839 let mut config = read_config_from_path(&get_user_config_path()?)?;
840
841 let registry =
842 if let Some(builtin) = crate::builtin::registry::get(url_or_handle) {
843 let authorities = authorities_for_builtin(&builtin);
844 Registry {
845 handle: builtin.handle.clone(),
846 url: builtin.git,
847 name: Some(builtin.name),
848 description: Some(builtin.description),
849 advisory_prefix: None,
850 authorities
851 }
852 } else {
853 Registry {
854 handle: String::new(),
855 url: url_or_handle.to_string(),
856 name: None,
857 description: None,
858 advisory_prefix: None,
859 authorities: None
860 }
861 };
862
863 config.default_registry = Some(registry);
864 write_user_config(&config)
865}
866
867pub fn set_user_default_registry(
873 default_registry: Option<Registry>
874) -> Result<()> {
875 let mut config = read_config_from_path(&get_user_config_path()?)?;
876 config.default_registry = default_registry;
877 write_user_config(&config)
878}
879
880pub fn add_added_registry(url_or_handle: &str) -> Result<()> {
892 let mut config = read_config_from_path(&get_user_config_path()?)?;
893
894 let (handle, url, name, description, authorities) =
895 if let Some(builtin) = crate::builtin::registry::get(url_or_handle) {
896 let authorities = authorities_for_builtin(&builtin);
897 (
898 Some(builtin.handle),
899 builtin.git,
900 Some(builtin.name),
901 Some(builtin.description),
902 authorities
903 )
904 } else {
905 (None, url_or_handle.to_string(), None, None, None)
906 };
907
908 push_added_registry(
909 &mut config,
910 Registry {
911 handle: handle.unwrap_or_default(),
912 url,
913 name,
914 description,
915 advisory_prefix: None,
916 authorities
917 },
918 url_or_handle
919 )?;
920
921 write_user_config(&config)
922}
923
924pub fn add_resolved_registry(registry: Registry) -> Result<()> {
936 if registry.handle.is_empty() {
937 return Err(anyhow!(
938 "Cannot add registry from '{}': it declares no 'name' to use as a \
939 handle.",
940 registry.url
941 ));
942 }
943
944 let mut config = read_config_from_path(&get_user_config_path()?)?;
945 let identity = registry.url.clone();
946 push_added_registry(&mut config, registry, &identity)?;
947
948 write_user_config(&config)
949}
950
951fn push_added_registry(
961 config: &mut Config,
962 registry: Registry,
963 identity: &str
964) -> Result<()> {
965 if config
966 .added_registries
967 .iter()
968 .any(|r| r.url == registry.url)
969 || (!registry.handle.is_empty()
970 && config
971 .added_registries
972 .iter()
973 .any(|r| r.handle == registry.handle))
974 {
975 return Err(anyhow!(
976 "Registry with URL or handle '{identity}' already exists."
977 ));
978 }
979
980 config.added_registries.push(registry);
981 Ok(())
982}
983
984pub fn remove_added_registry(handle_or_url: &str) -> Result<()> {
991 let mut config = read_config_from_path(&get_user_config_path()?)?;
992 if let Some(pos) = config
993 .added_registries
994 .iter()
995 .position(|r| r.handle == handle_or_url || r.url == handle_or_url)
996 {
997 let removed_registry = config.added_registries.remove(pos);
998 if !removed_registry.handle.is_empty() {
999 let db_root = get_db_root()?;
1000 let repo_path = db_root.join(removed_registry.handle);
1001 if repo_path.exists() {
1002 fs::remove_dir_all(repo_path)?;
1003 }
1004 }
1005 write_user_config(&config)
1006 } else {
1007 Err(anyhow!(
1008 "Added registry with handle or URL '{handle_or_url}' not found."
1009 ))
1010 }
1011}
1012
1013pub fn read_repo_config(db_path: &Path) -> Result<RepoConfig> {
1019 let config_path = crate::utils::safe_join(db_path, Path::new("repo.yaml"))?;
1020 if !config_path.exists() {
1021 return Err(anyhow!(
1022 "repo.yaml not found in the root of the package database."
1023 ));
1024 }
1025 let content = fs::read_to_string(config_path)?;
1026 let config: RepoConfig = serde_yaml::from_str(&content)?;
1027 Ok(config)
1028}
1029
1030pub fn read_user_config() -> Result<Config> {
1036 read_config_from_path(&get_user_config_path()?)
1037}
1038
1039pub fn update_global_versions<S: ::std::hash::BuildHasher>(
1045 versions: HashMap<String, String, S>
1046) -> Result<()> {
1047 let mut config = read_user_config()?;
1048 config.versions.extend(versions);
1049 write_user_config(&config)
1050}
1051
1052pub fn sync_remote_policy() -> Result<()> {
1067 let config = read_config()?;
1068 let Some(remote_cfg) = &config.remote_policy else {
1069 return Ok(());
1070 };
1071
1072 println!("{} Syncing remote security policy...", "::".bold().blue());
1073
1074 if crate::offline::is_offline() {
1075 return Err(anyhow!("Cannot sync remote policy in offline mode."));
1076 }
1077
1078 let client = crate::utils::get_http_client()?;
1079
1080 let policy_content = client.get(&remote_cfg.url).send()?.text()?;
1081 let sig_content = client.get(&remote_cfg.signature_url).send()?.bytes()?;
1082
1083 let trusted_certs =
1084 crate::pgp::get_certs_by_name_or_fingerprint(&remote_cfg.trusted_keys)?;
1085
1086 let temp_dir = tempfile::Builder::new().prefix("zoi-policy-").tempdir()?;
1087 let temp_policy_path = temp_dir.path().join("policy.yaml");
1088 let temp_sig_path = temp_dir.path().join("policy.yaml.sig");
1089
1090 fs::write(&temp_policy_path, &policy_content)?;
1091 fs::write(&temp_sig_path, &sig_content)?;
1092
1093 crate::pgp::verify_detached_signature_multi_key(
1094 &temp_policy_path,
1095 &temp_sig_path,
1096 trusted_certs
1097 )?;
1098
1099 let cache_path = get_remote_policy_cache_path();
1100 if let Some(parent) = cache_path.parent() {
1101 fs::create_dir_all(parent)?;
1102 }
1103 fs::write(cache_path, policy_content)?;
1104
1105 println!(
1106 "{}",
1107 "Remote policy verified and cached successfully.".green()
1108 );
1109 Ok(())
1110}
1111
1112fn merge_policies(
1118 base: &mut crate::types::Policy,
1119 remote: &crate::types::Policy
1120) {
1121 if remote.repos_unoverridable {
1122 base.repos_unoverridable = true;
1123 }
1124 if remote.telemetry_enabled_unoverridable {
1125 base.telemetry_enabled_unoverridable = true;
1126 }
1127 if remote.audit_log_enabled_unoverridable {
1128 base.audit_log_enabled_unoverridable = true;
1129 }
1130 if remote.rollback_enabled_unoverridable {
1131 base.rollback_enabled_unoverridable = true;
1132 }
1133 if remote.default_registry_unoverridable {
1134 base.default_registry_unoverridable = true;
1135 }
1136 if remote.added_registries_unoverridable {
1137 base.added_registries_unoverridable = true;
1138 }
1139 if remote.git_repos_unoverridable {
1140 base.git_repos_unoverridable = true;
1141 }
1142 if remote.allow_deny_lists_unoverridable {
1143 base.allow_deny_lists_unoverridable = true;
1144 }
1145 if remote.signature_enforcement_unoverridable {
1146 base.signature_enforcement_unoverridable = true;
1147 }
1148 if remote.protect_db_unoverridable {
1149 base.protect_db_unoverridable = true;
1150 }
1151 if remote.max_resolution_depth_unoverridable {
1152 base.max_resolution_depth_unoverridable = true;
1153 }
1154 if remote.offline_mode_unoverridable {
1155 base.offline_mode_unoverridable = true;
1156 }
1157 if remote.pkg_dirs_unoverridable {
1158 base.pkg_dirs_unoverridable = true;
1159 }
1160 if remote.cache_mirrors_unoverridable {
1161 base.cache_mirrors_unoverridable = true;
1162 }
1163 if remote.jobs_unoverridable {
1164 base.jobs_unoverridable = true;
1165 }
1166 if remote.advisory_enforcement_unoverridable {
1167 base.advisory_enforcement_unoverridable = true;
1168 }
1169
1170 if let Some(allowed) = &remote.allowed_licenses {
1171 base.allowed_licenses
1172 .get_or_insert_with(Vec::new)
1173 .extend(allowed.clone());
1174 }
1175 if let Some(denied) = &remote.denied_licenses {
1176 base.denied_licenses
1177 .get_or_insert_with(Vec::new)
1178 .extend(denied.clone());
1179 }
1180 if let Some(allowed) = &remote.allowed_packages {
1181 base.allowed_packages
1182 .get_or_insert_with(Vec::new)
1183 .extend(allowed.clone());
1184 }
1185 if let Some(denied) = &remote.denied_packages {
1186 base.denied_packages
1187 .get_or_insert_with(Vec::new)
1188 .extend(denied.clone());
1189 }
1190 if let Some(allowed) = &remote.allowed_repos {
1191 base.allowed_repos
1192 .get_or_insert_with(Vec::new)
1193 .extend(allowed.clone());
1194 }
1195 if let Some(denied) = &remote.denied_repos {
1196 base.denied_repos
1197 .get_or_insert_with(Vec::new)
1198 .extend(denied.clone());
1199 }
1200
1201 if let Some(remote_sig) = &remote.signature_enforcement {
1202 if let Some(ref mut base_sig) = base.signature_enforcement {
1203 if remote_sig.enable {
1204 base_sig.enable = true;
1205 }
1206 base_sig
1207 .trusted_keys
1208 .extend(remote_sig.trusted_keys.clone());
1209 base_sig.trusted_keys.sort();
1210 base_sig.trusted_keys.dedup();
1211 } else {
1212 base.signature_enforcement = Some(remote_sig.clone());
1213 }
1214 }
1215}