Skip to main content

zoi_core/
config.rs

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
14/// Returns the default registry URL for Zoi.
15///
16/// This is typically hardcoded or provided via an environment variable during
17/// the build.
18pub fn get_default_registry() -> String {
19    option_env!("ZOI_DEFAULT_REGISTRY")
20        .unwrap_or("community")
21        .to_string()
22}
23
24/// Returns the built-in PGP authorities for the default registry.
25pub 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
34/// Returns the path to the system-wide configuration file.
35fn 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
43/// Returns the path to the user-specific configuration file.
44fn 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
52/// Returns the path to the project-local configuration file.
53fn 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
60/// Returns the path to the directory where git repositories are cloned.
61fn 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
69/// Returns the path to the remote policy cache file.
70fn 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
78/// Reads a YAML value from the specified path.
79fn 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
87/// Reads a Zoi configuration from the specified path.
88fn 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
96/// Loads and merges configuration from system, user, and project-local paths.
97///
98/// Zoi uses a hierarchical configuration model with the following precedence:
99/// - System: (`/etc/zoi/config.yaml`) - Defines global machine policy.
100/// - User: (`~/.zoi/pkgs/config.yaml`) - Defines user preferences.
101/// - Project: (`./.zoi/pkgs/config.yaml`) - Local overrides for a specific
102///   project.
103///
104/// Policy Enforcement: If a field is marked as `unoverridable` in the
105/// system-level policy, Zoi will ignore any overrides found in user or project
106/// configs.
107///
108/// # Errors
109///
110/// Returns an error if any of the configuration files cannot be read or parsed.
111pub 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(&reg.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
430/// Writes the user-specific configuration to disk.
431///
432/// # Errors
433///
434/// Returns an error if the configuration directory cannot be created or the
435/// file cannot be written.
436pub 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
447/// Verifies a remote file against a set of trusted PGP keys.
448///
449/// # Errors
450///
451/// Returns an error if:
452/// - The file or its signature cannot be fetched.
453/// - The signature is invalid or not signed by a trusted key.
454pub 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
498/// Adds a repository to the user-specific configuration.
499///
500/// # Errors
501///
502/// Returns an error if the repository already exists or if the configuration
503/// cannot be written.
504pub 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
516/// Removes a repository from the user-specific configuration.
517///
518/// # Errors
519///
520/// Returns an error if the repository is not found or if the configuration
521/// cannot be written.
522pub 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
535/// Prompts the user to interactively add a repository.
536///
537/// # Errors
538///
539/// Returns an error if:
540/// - Input/output operations fail.
541/// - The user provides invalid input.
542/// - The repository cannot be added.
543pub 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
595/// Returns a list of all available repositories from the default registry.
596///
597/// # Errors
598///
599/// Returns an error if the repository configuration cannot be read.
600pub 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
617/// Clones a git repository into the local git root.
618///
619/// # Errors
620///
621/// Returns an error if:
622/// - The target directory already exists.
623/// - The `git clone` command fails.
624/// - The user configuration cannot be updated.
625pub 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
666/// Lists all cloned git repositories.
667///
668/// # Errors
669///
670/// Returns an error if the git root directory cannot be read.
671pub 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
688/// Removes a cloned git repository.
689///
690/// # Errors
691///
692/// Returns an error if the repository is not found or cannot be removed.
693pub 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
730/// Adds a cache mirror URL to the user-specific configuration.
731///
732/// # Errors
733///
734/// Returns an error if the mirror already exists or the configuration cannot be
735/// written.
736pub 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
745/// Removes a cache mirror URL from the user-specific configuration.
746///
747/// # Errors
748///
749/// Returns an error if the mirror is not found or the configuration cannot be
750/// written.
751pub 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
765/// Sets the default registry URL in the user-specific configuration.
766///
767/// # Errors
768///
769/// Returns an error if the configuration cannot be written.
770pub 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
781/// Sets the default registry in the user-specific configuration.
782///
783/// # Errors
784///
785/// Returns an error if the configuration cannot be written.
786pub 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
794/// Adds a registry URL to the list of added registries in the user-specific
795/// configuration.
796///
797/// # Errors
798///
799/// Returns an error if the registry already exists or the configuration cannot
800/// be written.
801pub 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
815/// Removes an added registry by its handle or URL.
816///
817/// # Errors
818///
819/// Returns an error if the registry is not found or the configuration cannot be
820/// updated.
821pub 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
844/// Reads the repository configuration from the specified database path.
845///
846/// # Errors
847///
848/// Returns an error if `repo.yaml` is not found or cannot be parsed.
849pub 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
861/// Reads the user-specific configuration.
862///
863/// # Errors
864///
865/// Returns an error if the configuration cannot be read or parsed.
866pub fn read_user_config() -> Result<Config> {
867    read_config_from_path(&get_user_config_path()?)
868}
869
870/// Updates the global package versions in the user-specific configuration.
871///
872/// # Errors
873///
874/// Returns an error if the configuration cannot be read or written.
875pub 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
883/// Synchronizes the remote security policy if configured in system
884/// `config.yaml`.
885///
886/// This allows organizations to manage security constraints (allowed/denied
887/// lists) centrally. It verifies the policy's PGP signature before caching it
888/// locally.
889///
890/// # Errors
891///
892/// Returns an error if:
893/// - The configuration cannot be read.
894/// - The system is in offline mode and a sync is required.
895/// - The remote policy or its signature cannot be downloaded.
896/// - The signature verification fails.
897pub 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
943/// Merges a remote security policy into the local machine policy.
944///
945/// Precedence: If a field is already unoverridable in the base (machine)
946/// policy, it remains so. Otherwise, the remote policy's unoverridable flags
947/// and lists are added/merged into the local state.
948fn 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}