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_home};
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 if cfg!(target_os = "windows") {
37 apply_sysroot(PathBuf::from("C:\\ProgramData\\zoi\\config.yaml"))
38 } else {
39 apply_sysroot(PathBuf::from("/etc/zoi/config.yaml"))
40 }
41}
42
43fn get_user_config_path() -> Result<PathBuf> {
45 let home_dir = get_user_home()
46 .ok_or_else(|| anyhow!("Could not find home directory."))?;
47 Ok(apply_sysroot(home_dir.join(".zoi"))
48 .join("pkgs")
49 .join("config.yaml"))
50}
51
52fn get_project_config_path() -> Result<PathBuf> {
54 let current_dir = std::env::current_dir()?;
55 Ok(apply_sysroot(current_dir.join(".zoi"))
56 .join("pkgs")
57 .join("config.yaml"))
58}
59
60fn get_git_root() -> Result<PathBuf> {
62 let home_dir = get_user_home()
63 .ok_or_else(|| anyhow!("Could not find home directory."))?;
64 Ok(apply_sysroot(home_dir.join(".zoi"))
65 .join("pkgs")
66 .join("git"))
67}
68
69fn get_remote_policy_cache_path() -> PathBuf {
71 if cfg!(target_os = "windows") {
72 apply_sysroot(PathBuf::from("C:\\ProgramData\\zoi\\policy.cache.yaml"))
73 } else {
74 apply_sysroot(PathBuf::from("/etc/zoi/policy.cache.yaml"))
75 }
76}
77
78fn read_yaml_value(path: &Path) -> Result<Value> {
80 if !path.exists() {
81 return Ok(Value::Null);
82 }
83 let content = fs::read_to_string(path)?;
84 serde_yaml::from_str(&content).map_err(Into::into)
85}
86
87fn read_config_from_path(path: &Path) -> Result<Config> {
89 if !path.exists() {
90 return Ok(Config::default());
91 }
92 let content = fs::read_to_string(path)?;
93 serde_yaml::from_str(&content).map_err(Into::into)
94}
95
96pub fn read_config() -> Result<Config> {
112 let system_val = read_yaml_value(&get_system_config_path())?;
113 let user_val = read_yaml_value(&get_user_config_path()?)?;
114 let project_val = read_yaml_value(&get_project_config_path()?)?;
115
116 let mut system_cfg: Config =
117 serde_yaml::from_value(system_val.clone()).unwrap_or_default();
118 let user_cfg: Config =
119 serde_yaml::from_value(user_val.clone()).unwrap_or_default();
120 let project_cfg: Config =
121 serde_yaml::from_value(project_val.clone()).unwrap_or_default();
122
123 let cache_path = get_remote_policy_cache_path();
124 if cache_path.exists()
125 && let Ok(cache_content) = fs::read_to_string(&cache_path)
126 && let Ok(remote_policy) =
127 serde_yaml::from_str::<crate::types::Policy>(&cache_content)
128 {
129 merge_policies(&mut system_cfg.policy, &remote_policy);
130 }
131
132 let system_policy = system_cfg.policy.clone();
133 let mut merged_cfg = Config {
134 policy: system_policy.clone(),
135 ..Default::default()
136 };
137
138 merged_cfg.repos = system_cfg.repos;
139 if !system_policy.repos_unoverridable {
140 merged_cfg.repos.extend(user_cfg.repos);
141 merged_cfg.repos.extend(project_cfg.repos);
142 }
143 merged_cfg.repos.sort();
144 merged_cfg.repos.dedup();
145
146 merged_cfg.added_registries = system_cfg.added_registries;
147 if !system_policy.added_registries_unoverridable {
148 merged_cfg
149 .added_registries
150 .extend(user_cfg.added_registries);
151 merged_cfg
152 .added_registries
153 .extend(project_cfg.added_registries);
154 }
155 let mut seen_registries = HashSet::new();
156 merged_cfg
157 .added_registries
158 .retain(|r| seen_registries.insert(r.url.clone()));
159
160 merged_cfg.git_repos = system_cfg.git_repos;
161 if !system_policy.git_repos_unoverridable {
162 merged_cfg.git_repos.extend(user_cfg.git_repos);
163 merged_cfg.git_repos.extend(project_cfg.git_repos);
164 }
165 merged_cfg.git_repos.sort();
166 merged_cfg.git_repos.dedup();
167
168 merged_cfg.package_managers = project_cfg
169 .package_managers
170 .or(user_cfg.package_managers)
171 .or(system_cfg.package_managers);
172 merged_cfg.native_package_manager = project_cfg
173 .native_package_manager
174 .or(user_cfg.native_package_manager)
175 .or(system_cfg.native_package_manager);
176 merged_cfg.registry = project_cfg
177 .registry
178 .or(user_cfg.registry)
179 .or(system_cfg.registry);
180
181 merged_cfg.remote_policy = system_cfg.remote_policy;
182
183 if project_val.get("telemetry_enabled").is_some()
184 && !system_policy.telemetry_enabled_unoverridable
185 {
186 merged_cfg.telemetry_enabled = project_cfg.telemetry_enabled;
187 } else if user_val.get("telemetry_enabled").is_some()
188 && !system_policy.telemetry_enabled_unoverridable
189 {
190 merged_cfg.telemetry_enabled = user_cfg.telemetry_enabled;
191 } else {
192 merged_cfg.telemetry_enabled = system_cfg.telemetry_enabled;
193 }
194
195 if project_val.get("audit_log_enabled").is_some()
196 && !system_policy.audit_log_enabled_unoverridable
197 {
198 merged_cfg.audit_log_enabled = project_cfg.audit_log_enabled;
199 } else if user_val.get("audit_log_enabled").is_some()
200 && !system_policy.audit_log_enabled_unoverridable
201 {
202 merged_cfg.audit_log_enabled = user_cfg.audit_log_enabled;
203 } else {
204 merged_cfg.audit_log_enabled = system_cfg.audit_log_enabled;
205 }
206
207 if project_val.get("rollback_enabled").is_some()
208 && !system_policy.rollback_enabled_unoverridable
209 {
210 merged_cfg.rollback_enabled = project_cfg.rollback_enabled;
211 } else if user_val.get("rollback_enabled").is_some()
212 && !system_policy.rollback_enabled_unoverridable
213 {
214 merged_cfg.rollback_enabled = user_cfg.rollback_enabled;
215 } else {
216 merged_cfg.rollback_enabled = system_cfg.rollback_enabled;
217 }
218
219 if project_val.get("default_registry").is_some()
220 && !system_policy.default_registry_unoverridable
221 {
222 merged_cfg.default_registry = project_cfg.default_registry;
223 } else if user_val.get("default_registry").is_some()
224 && !system_policy.default_registry_unoverridable
225 {
226 merged_cfg.default_registry = user_cfg.default_registry;
227 } else {
228 merged_cfg.default_registry = system_cfg.default_registry;
229 }
230
231 if project_val.get("jobs").is_some() && !system_policy.jobs_unoverridable {
232 merged_cfg.jobs = project_cfg.jobs;
233 } else if user_val.get("jobs").is_some()
234 && !system_policy.jobs_unoverridable
235 {
236 merged_cfg.jobs = user_cfg.jobs;
237 } else {
238 merged_cfg.jobs = system_cfg.jobs;
239 }
240
241 if project_val.get("protect_db").is_some()
242 && !system_policy.protect_db_unoverridable
243 {
244 merged_cfg.protect_db = project_cfg.protect_db;
245 } else if user_val.get("protect_db").is_some()
246 && !system_policy.protect_db_unoverridable
247 {
248 merged_cfg.protect_db = user_cfg.protect_db;
249 } else {
250 merged_cfg.protect_db = system_cfg.protect_db;
251 }
252
253 if project_val.get("max_resolution_depth").is_some()
254 && !system_policy.max_resolution_depth_unoverridable
255 {
256 merged_cfg.max_resolution_depth = project_cfg.max_resolution_depth;
257 } else if user_val.get("max_resolution_depth").is_some()
258 && !system_policy.max_resolution_depth_unoverridable
259 {
260 merged_cfg.max_resolution_depth = user_cfg.max_resolution_depth;
261 } else {
262 merged_cfg.max_resolution_depth = system_cfg.max_resolution_depth;
263 }
264
265 if project_val.get("offline_mode").is_some()
266 && !system_policy.offline_mode_unoverridable
267 {
268 merged_cfg.offline_mode = project_cfg.offline_mode;
269 } else if user_val.get("offline_mode").is_some()
270 && !system_policy.offline_mode_unoverridable
271 {
272 merged_cfg.offline_mode = user_cfg.offline_mode;
273 } else {
274 merged_cfg.offline_mode = system_cfg.offline_mode;
275 }
276
277 if project_val
278 .get("policy")
279 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
280 .is_some()
281 && !system_policy.advisory_enforcement_unoverridable
282 {
283 merged_cfg.policy.advisory_enforcement_unoverridable =
284 project_cfg.policy.advisory_enforcement_unoverridable;
285 } else if user_val
286 .get("policy")
287 .and_then(|p| p.get("advisory_enforcement_unoverridable"))
288 .is_some()
289 && !system_policy.advisory_enforcement_unoverridable
290 {
291 merged_cfg.policy.advisory_enforcement_unoverridable =
292 user_cfg.policy.advisory_enforcement_unoverridable;
293 } else {
294 merged_cfg.policy.advisory_enforcement_unoverridable =
295 system_cfg.policy.advisory_enforcement_unoverridable;
296 }
297
298 merged_cfg.versions = system_cfg.versions;
299 merged_cfg.versions.extend(user_cfg.versions);
300 merged_cfg.versions.extend(project_cfg.versions);
301
302 merged_cfg.pkg_dirs = system_cfg.pkg_dirs;
303 if !system_policy.pkg_dirs_unoverridable {
304 merged_cfg.pkg_dirs.extend(user_cfg.pkg_dirs);
305 merged_cfg.pkg_dirs.extend(project_cfg.pkg_dirs);
306 }
307 merged_cfg.pkg_dirs.sort();
308 merged_cfg.pkg_dirs.dedup();
309
310 merged_cfg.cache_mirrors = system_cfg.cache_mirrors;
311 if !system_policy.cache_mirrors_unoverridable {
312 merged_cfg.cache_mirrors.extend(user_cfg.cache_mirrors);
313 merged_cfg.cache_mirrors.extend(project_cfg.cache_mirrors);
314 }
315 merged_cfg.cache_mirrors.sort();
316 merged_cfg.cache_mirrors.dedup();
317
318 if !system_policy.allow_deny_lists_unoverridable {
319 if project_cfg.policy.allowed_licenses.is_some() {
320 merged_cfg.policy.allowed_licenses =
321 project_cfg.policy.allowed_licenses;
322 } else if user_cfg.policy.allowed_licenses.is_some() {
323 merged_cfg.policy.allowed_licenses =
324 user_cfg.policy.allowed_licenses;
325 }
326 if project_cfg.policy.denied_licenses.is_some() {
327 merged_cfg.policy.denied_licenses =
328 project_cfg.policy.denied_licenses;
329 } else if user_cfg.policy.denied_licenses.is_some() {
330 merged_cfg.policy.denied_licenses = user_cfg.policy.denied_licenses;
331 }
332 if project_cfg.policy.allowed_packages.is_some() {
333 merged_cfg.policy.allowed_packages =
334 project_cfg.policy.allowed_packages;
335 } else if user_cfg.policy.allowed_packages.is_some() {
336 merged_cfg.policy.allowed_packages =
337 user_cfg.policy.allowed_packages;
338 }
339 if project_cfg.policy.denied_packages.is_some() {
340 merged_cfg.policy.denied_packages =
341 project_cfg.policy.denied_packages;
342 } else if user_cfg.policy.denied_packages.is_some() {
343 merged_cfg.policy.denied_packages = user_cfg.policy.denied_packages;
344 }
345 if project_cfg.policy.allowed_repos.is_some() {
346 merged_cfg.policy.allowed_repos = project_cfg.policy.allowed_repos;
347 } else if user_cfg.policy.allowed_repos.is_some() {
348 merged_cfg.policy.allowed_repos = user_cfg.policy.allowed_repos;
349 }
350 if project_cfg.policy.denied_repos.is_some() {
351 merged_cfg.policy.denied_repos = project_cfg.policy.denied_repos;
352 } else if user_cfg.policy.denied_repos.is_some() {
353 merged_cfg.policy.denied_repos = user_cfg.policy.denied_repos;
354 }
355 }
356
357 if !system_policy.signature_enforcement_unoverridable {
358 if project_cfg.policy.signature_enforcement.is_some() {
359 merged_cfg.policy.signature_enforcement =
360 project_cfg.policy.signature_enforcement;
361 } else if user_cfg.policy.signature_enforcement.is_some() {
362 merged_cfg.policy.signature_enforcement =
363 user_cfg.policy.signature_enforcement;
364 }
365 }
366
367 if let Some(url) = merged_cfg.registry.take()
368 && merged_cfg.default_registry.is_none()
369 {
370 merged_cfg.default_registry = Some(Registry {
371 handle: String::new(),
372 url,
373 advisory_prefix: None,
374 authorities: None
375 });
376 }
377
378 if merged_cfg.default_registry.is_none() {
379 merged_cfg.default_registry = Some(Registry {
380 handle: "zoidberg".to_string(),
381 url: get_default_registry(),
382 advisory_prefix: Some("ZSA".to_string()),
383 authorities: Some(get_builtin_authorities())
384 });
385 } else if let Some(ref mut reg) = merged_cfg.default_registry
386 && reg.url == get_default_registry()
387 && reg.authorities.as_ref().is_none_or(Vec::is_empty)
388 {
389 let builtin = get_builtin_authorities();
390 if !builtin.is_empty() {
391 reg.authorities = Some(builtin);
392 }
393 }
394
395 if merged_cfg.repos.is_empty()
396 && let Some(reg) = &merged_cfg.default_registry
397 && !reg.handle.is_empty()
398 {
399 let db_root = get_db_root()?;
400 let repo_path = db_root.join(®.handle);
401 if repo_path.join("repo.yaml").exists()
402 && let Ok(repo_config) = read_repo_config(&repo_path)
403 {
404 merged_cfg.repos = repo_config
405 .repos
406 .into_iter()
407 .filter(|r| r.active)
408 .map(|r| r.name)
409 .collect();
410 }
411 }
412
413 if project_val.get("system_generations_limit").is_some()
414 && !system_policy.system_generations_limit_unoverridable
415 {
416 merged_cfg.system_generations_limit =
417 project_cfg.system_generations_limit;
418 } else if user_val.get("system_generations_limit").is_some()
419 && !system_policy.system_generations_limit_unoverridable
420 {
421 merged_cfg.system_generations_limit = user_cfg.system_generations_limit;
422 } else {
423 merged_cfg.system_generations_limit =
424 system_cfg.system_generations_limit;
425 }
426
427 Ok(merged_cfg)
428}
429
430pub fn write_user_config(config: &Config) -> Result<()> {
437 let config_path = get_user_config_path()?;
438 let parent_dir = config_path
439 .parent()
440 .ok_or_else(|| anyhow!("Invalid config path"))?;
441 fs::create_dir_all(parent_dir)?;
442 let content = serde_yaml::to_string(config)?;
443 fs::write(config_path, content)?;
444 Ok(())
445}
446
447pub fn verify_remote_file(
455 url: &str,
456 trusted_keys: &[String]
457) -> Result<Vec<u8>> {
458 let client = crate::utils::get_http_client()?;
459
460 let response = client.get(url).send()?;
461 if !response.status().is_success() {
462 return Err(anyhow!(
463 "Failed to fetch remote file '{url}': {}",
464 response.status()
465 ));
466 }
467 let data = response.bytes()?;
468
469 let sig_url = format!("{url}.sig");
470 let sig_response = client.get(&sig_url).send()?;
471 if !sig_response.status().is_success() {
472 return Err(anyhow!(
473 "Failed to fetch signature for '{url}': {}",
474 sig_response.status()
475 ));
476 }
477 let sig = sig_response.bytes()?;
478
479 let trusted_certs =
480 crate::pgp::get_certs_by_name_or_fingerprint(trusted_keys)?;
481
482 let temp_dir = tempfile::Builder::new().prefix("zoi-verify-").tempdir()?;
483 let data_path = temp_dir.path().join("data");
484 let sig_path = temp_dir.path().join("sig");
485
486 fs::write(&data_path, &data)?;
487 fs::write(&sig_path, &sig)?;
488
489 crate::pgp::verify_detached_signature_multi_key(
490 &data_path,
491 &sig_path,
492 trusted_certs
493 )?;
494
495 Ok(data.to_vec())
496}
497
498pub fn add_repo(repo_name: &str) -> Result<()> {
505 let mut config = read_config_from_path(&get_user_config_path()?)?;
506 let lower_repo_name = repo_name.to_lowercase();
507 if config.repos.contains(&lower_repo_name) {
508 return Err(anyhow!(
509 "Repository '{repo_name}' already exists in user config."
510 ));
511 }
512 config.repos.push(lower_repo_name);
513 write_user_config(&config)
514}
515
516pub fn remove_repo(repo_name: &str) -> Result<()> {
523 let mut config = read_config_from_path(&get_user_config_path()?)?;
524 let lower_repo_name = repo_name.to_lowercase();
525 if let Some(pos) = config.repos.iter().position(|r| r == &lower_repo_name) {
526 config.repos.remove(pos);
527 write_user_config(&config)
528 } else {
529 Err(anyhow!(
530 "Repository '{repo_name}' not found in user config."
531 ))
532 }
533}
534
535pub fn interactive_add_repo() -> Result<()> {
544 let config = read_config()?;
545 let all_repos = get_all_repos()?;
546
547 let available_repos: Vec<_> = all_repos
548 .into_iter()
549 .filter(|r| !config.repos.contains(&r.to_lowercase()))
550 .collect();
551
552 if available_repos.is_empty() {
553 println!("{}", "No new repositories available to add.".yellow());
554 return Ok(());
555 }
556
557 println!("{}", "Available repositories to add:".green());
558 for (i, repo) in available_repos.iter().enumerate() {
559 println!("[{}] {}", i + 1, repo);
560 }
561
562 print!(
563 "\n{}",
564 "Select a repository to add (or 'q' to quit): ".yellow()
565 );
566 io::stdout().flush()?;
567
568 let mut input = String::new();
569 io::stdin().read_line(&mut input)?;
570 let input = input.trim();
571
572 if input == "q" {
573 println!("Aborted.");
574 return Ok(());
575 }
576
577 let choice: usize = match input.parse() {
578 Ok(num) => num,
579 Err(_) => return Err(anyhow!("Invalid input."))
580 };
581
582 if choice > 0 && choice <= available_repos.len() {
583 let repo_to_add = available_repos
584 .get(choice - 1)
585 .ok_or_else(|| anyhow!("Invalid choice"))?;
586 add_repo(repo_to_add)?;
587 println!("Repository '{}' added successfully.", repo_to_add.green());
588 } else {
589 return Err(anyhow!("Invalid selection."));
590 }
591
592 Ok(())
593}
594
595pub fn get_all_repos() -> Result<Vec<String>> {
601 let db_root = get_db_root()?;
602 let config = read_config()?;
603
604 if let Some(default_reg) = config.default_registry
605 && !default_reg.handle.is_empty()
606 {
607 let default_reg_path = db_root.join(default_reg.handle);
608 if default_reg_path.join("repo.yaml").exists() {
609 let repo_config = read_repo_config(&default_reg_path)?;
610 return Ok(repo_config.repos.into_iter().map(|r| r.name).collect());
611 }
612 }
613
614 Ok(Vec::new())
615}
616
617pub fn clone_git_repo(url: &str) -> Result<()> {
626 let git_root = get_git_root()?;
627 fs::create_dir_all(&git_root)?;
628 let repo_name = url
629 .trim_end_matches('/')
630 .split('/')
631 .next_back()
632 .unwrap_or("repo")
633 .trim_end_matches(".git");
634 let target = git_root.join(repo_name);
635 if target.exists() {
636 return Err(anyhow!(
637 "Git repo '{repo_name}' already exists at {}",
638 target.display()
639 ));
640 }
641 println!("Cloning '{}' into {}...", url.cyan(), target.display());
642 let status = std::process::Command::new("git")
643 .arg("clone")
644 .arg("--depth=1")
645 .arg(url)
646 .arg(&target)
647 .status()?;
648 if !status.success() {
649 return Err(anyhow!("git clone failed"));
650 }
651
652 let mut config = read_config_from_path(&get_user_config_path()?)?;
653 if !config.git_repos.iter().any(|repo_url| repo_url == url) {
654 config.git_repos.push(url.to_string());
655 write_user_config(&config)?;
656 }
657
658 println!(
659 "Cloned git repo as '{}' (use with '#git@{}/<pkg>')",
660 repo_name.green(),
661 repo_name
662 );
663 Ok(())
664}
665
666pub fn list_git_repos() -> Result<Vec<String>> {
672 let git_root = get_git_root()?;
673 if !git_root.exists() {
674 return Ok(Vec::new());
675 }
676
677 let mut repos = Vec::new();
678 for entry in fs::read_dir(git_root)? {
679 let entry = entry?;
680 if entry.path().is_dir() {
681 repos.push(entry.file_name().to_string_lossy().into_owned());
682 }
683 }
684 repos.sort();
685 Ok(repos)
686}
687
688pub fn remove_git_repo(repo_name: &str) -> Result<()> {
694 let git_root = get_git_root()?;
695 let target = git_root.join(repo_name);
696 if !target.exists() {
697 return Err(anyhow!("Git repository '{repo_name}' not found."));
698 }
699
700 let mut config = read_config_from_path(&get_user_config_path()?)?;
701 let mut removed = false;
702 config.git_repos.retain(|url| {
703 let name_from_url = url
704 .trim_end_matches('/')
705 .split('/')
706 .next_back()
707 .unwrap_or_default()
708 .trim_end_matches(".git");
709 if name_from_url == repo_name {
710 removed = true;
711 false
712 } else {
713 true
714 }
715 });
716
717 if removed {
718 write_user_config(&config)?;
719 }
720
721 fs::remove_dir_all(&target)?;
722 println!(
723 "Removed git repository '{}' from {}",
724 repo_name.green(),
725 target.display()
726 );
727 Ok(())
728}
729
730pub fn add_cache_mirror(url: &str) -> Result<()> {
737 let mut config = read_config_from_path(&get_user_config_path()?)?;
738 if config.cache_mirrors.iter().any(|existing| existing == url) {
739 return Err(anyhow!("Cache mirror '{url}' already exists."));
740 }
741 config.cache_mirrors.push(url.to_string());
742 write_user_config(&config)
743}
744
745pub fn remove_cache_mirror(url: &str) -> Result<()> {
752 let mut config = read_config_from_path(&get_user_config_path()?)?;
753 if let Some(pos) = config
754 .cache_mirrors
755 .iter()
756 .position(|existing| existing == url)
757 {
758 config.cache_mirrors.remove(pos);
759 write_user_config(&config)
760 } else {
761 Err(anyhow!("Cache mirror '{url}' not found."))
762 }
763}
764
765pub fn set_default_registry(url: &str) -> Result<()> {
771 let mut config = read_config_from_path(&get_user_config_path()?)?;
772 config.default_registry = Some(Registry {
773 handle: String::new(),
774 url: url.to_string(),
775 advisory_prefix: None,
776 authorities: None
777 });
778 write_user_config(&config)
779}
780
781pub fn set_user_default_registry(
787 default_registry: Option<Registry>
788) -> Result<()> {
789 let mut config = read_config_from_path(&get_user_config_path()?)?;
790 config.default_registry = default_registry;
791 write_user_config(&config)
792}
793
794pub fn add_added_registry(url: &str) -> Result<()> {
802 let mut config = read_config_from_path(&get_user_config_path()?)?;
803 if config.added_registries.iter().any(|r| r.url == url) {
804 return Err(anyhow!("Registry with URL '{url}' already exists."));
805 }
806 config.added_registries.push(Registry {
807 handle: String::new(),
808 url: url.to_string(),
809 advisory_prefix: None,
810 authorities: None
811 });
812 write_user_config(&config)
813}
814
815pub fn remove_added_registry(handle_or_url: &str) -> Result<()> {
822 let mut config = read_config_from_path(&get_user_config_path()?)?;
823 if let Some(pos) = config
824 .added_registries
825 .iter()
826 .position(|r| r.handle == handle_or_url || r.url == handle_or_url)
827 {
828 let removed_registry = config.added_registries.remove(pos);
829 if !removed_registry.handle.is_empty() {
830 let db_root = get_db_root()?;
831 let repo_path = db_root.join(removed_registry.handle);
832 if repo_path.exists() {
833 fs::remove_dir_all(repo_path)?;
834 }
835 }
836 write_user_config(&config)
837 } else {
838 Err(anyhow!(
839 "Added registry with handle or URL '{handle_or_url}' not found."
840 ))
841 }
842}
843
844pub fn read_repo_config(db_path: &Path) -> Result<RepoConfig> {
850 let config_path = db_path.join("repo.yaml");
851 if !config_path.exists() {
852 return Err(anyhow!(
853 "repo.yaml not found in the root of the package database."
854 ));
855 }
856 let content = fs::read_to_string(config_path)?;
857 let config: RepoConfig = serde_yaml::from_str(&content)?;
858 Ok(config)
859}
860
861pub fn read_user_config() -> Result<Config> {
867 read_config_from_path(&get_user_config_path()?)
868}
869
870pub fn update_global_versions<S: ::std::hash::BuildHasher>(
876 versions: HashMap<String, String, S>
877) -> Result<()> {
878 let mut config = read_user_config()?;
879 config.versions.extend(versions);
880 write_user_config(&config)
881}
882
883pub fn sync_remote_policy() -> Result<()> {
898 let config = read_config()?;
899 let Some(remote_cfg) = &config.remote_policy else {
900 return Ok(());
901 };
902
903 println!("{} Syncing remote security policy...", "::".bold().blue());
904
905 if crate::offline::is_offline() {
906 return Err(anyhow!("Cannot sync remote policy in offline mode."));
907 }
908
909 let client = crate::utils::get_http_client()?;
910
911 let policy_content = client.get(&remote_cfg.url).send()?.text()?;
912 let sig_content = client.get(&remote_cfg.signature_url).send()?.bytes()?;
913
914 let trusted_certs =
915 crate::pgp::get_certs_by_name_or_fingerprint(&remote_cfg.trusted_keys)?;
916
917 let temp_dir = tempfile::Builder::new().prefix("zoi-policy-").tempdir()?;
918 let temp_policy_path = temp_dir.path().join("policy.yaml");
919 let temp_sig_path = temp_dir.path().join("policy.yaml.sig");
920
921 fs::write(&temp_policy_path, &policy_content)?;
922 fs::write(&temp_sig_path, &sig_content)?;
923
924 crate::pgp::verify_detached_signature_multi_key(
925 &temp_policy_path,
926 &temp_sig_path,
927 trusted_certs
928 )?;
929
930 let cache_path = get_remote_policy_cache_path();
931 if let Some(parent) = cache_path.parent() {
932 fs::create_dir_all(parent)?;
933 }
934 fs::write(cache_path, policy_content)?;
935
936 println!(
937 "{}",
938 "Remote policy verified and cached successfully.".green()
939 );
940 Ok(())
941}
942
943fn merge_policies(
949 base: &mut crate::types::Policy,
950 remote: &crate::types::Policy
951) {
952 if remote.repos_unoverridable {
953 base.repos_unoverridable = true;
954 }
955 if remote.telemetry_enabled_unoverridable {
956 base.telemetry_enabled_unoverridable = true;
957 }
958 if remote.audit_log_enabled_unoverridable {
959 base.audit_log_enabled_unoverridable = true;
960 }
961 if remote.rollback_enabled_unoverridable {
962 base.rollback_enabled_unoverridable = true;
963 }
964 if remote.default_registry_unoverridable {
965 base.default_registry_unoverridable = true;
966 }
967 if remote.added_registries_unoverridable {
968 base.added_registries_unoverridable = true;
969 }
970 if remote.git_repos_unoverridable {
971 base.git_repos_unoverridable = true;
972 }
973 if remote.allow_deny_lists_unoverridable {
974 base.allow_deny_lists_unoverridable = true;
975 }
976 if remote.signature_enforcement_unoverridable {
977 base.signature_enforcement_unoverridable = true;
978 }
979 if remote.protect_db_unoverridable {
980 base.protect_db_unoverridable = true;
981 }
982 if remote.max_resolution_depth_unoverridable {
983 base.max_resolution_depth_unoverridable = true;
984 }
985 if remote.offline_mode_unoverridable {
986 base.offline_mode_unoverridable = true;
987 }
988 if remote.pkg_dirs_unoverridable {
989 base.pkg_dirs_unoverridable = true;
990 }
991 if remote.cache_mirrors_unoverridable {
992 base.cache_mirrors_unoverridable = true;
993 }
994 if remote.jobs_unoverridable {
995 base.jobs_unoverridable = true;
996 }
997 if remote.advisory_enforcement_unoverridable {
998 base.advisory_enforcement_unoverridable = true;
999 }
1000
1001 if let Some(allowed) = &remote.allowed_licenses {
1002 base.allowed_licenses
1003 .get_or_insert_with(Vec::new)
1004 .extend(allowed.clone());
1005 }
1006 if let Some(denied) = &remote.denied_licenses {
1007 base.denied_licenses
1008 .get_or_insert_with(Vec::new)
1009 .extend(denied.clone());
1010 }
1011 if let Some(allowed) = &remote.allowed_packages {
1012 base.allowed_packages
1013 .get_or_insert_with(Vec::new)
1014 .extend(allowed.clone());
1015 }
1016 if let Some(denied) = &remote.denied_packages {
1017 base.denied_packages
1018 .get_or_insert_with(Vec::new)
1019 .extend(denied.clone());
1020 }
1021 if let Some(allowed) = &remote.allowed_repos {
1022 base.allowed_repos
1023 .get_or_insert_with(Vec::new)
1024 .extend(allowed.clone());
1025 }
1026 if let Some(denied) = &remote.denied_repos {
1027 base.denied_repos
1028 .get_or_insert_with(Vec::new)
1029 .extend(denied.clone());
1030 }
1031
1032 if let Some(remote_sig) = &remote.signature_enforcement {
1033 if let Some(ref mut base_sig) = base.signature_enforcement {
1034 if remote_sig.enable {
1035 base_sig.enable = true;
1036 }
1037 base_sig
1038 .trusted_keys
1039 .extend(remote_sig.trusted_keys.clone());
1040 base_sig.trusted_keys.sort();
1041 base_sig.trusted_keys.dedup();
1042 } else {
1043 base.signature_enforcement = Some(remote_sig.clone());
1044 }
1045 }
1046}