Skip to main content

dnf_repofile/
validate.rs

1//! Validation engine for checking DNF repository configuration consistency.
2//!
3//! Provides [`ValidationReport`] as the top-level result type, containing
4//! separate lists of errors and warnings. Each finding is a [`ValidationIssue`]
5//! with a severity level ([`IssueLevel`]), a location ([`IssueLocation`]), an
6//! optional field name, and a human-readable message.
7//!
8//! # Current validation rules
9//!
10//! - **Error**: repo missing URL source (no `baseurl`, `mirrorlist`, or `metalink`)
11//! - **Warning**: both `baseurl` and `mirrorlist`/`metalink` are set
12//! - **Warning**: `gpgkey` is set but neither `gpgcheck` nor `repo_gpgcheck` is enabled
13
14use crate::mainconfig::MainConfig;
15use crate::repo::Repo;
16use crate::types::{DnfBool, RepoId};
17
18/// Report containing validation errors and warnings.
19///
20/// Use [`is_ok()`](ValidationReport::is_ok) to check if the configuration is
21/// valid (no errors). Warnings alone do not indicate invalidity.
22///
23/// # Examples
24///
25/// ```
26/// use dnf_repofile::{Repo, RepoId, ValidationReport};
27///
28/// // A repo with no URL source is invalid
29/// let repo = Repo::new(RepoId::try_new("test").unwrap());
30/// let report = repo.validate();
31/// assert!(!report.is_ok());
32/// assert_eq!(report.errors.len(), 1);
33/// ```
34#[derive(Debug, Clone)]
35pub struct ValidationReport {
36    /// Issues classified as errors (configuration is invalid).
37    pub errors: Vec<ValidationIssue>,
38    /// Issues classified as warnings (advisory, non-fatal).
39    pub warnings: Vec<ValidationIssue>,
40}
41
42/// A single validation finding with severity level and location.
43///
44/// Each issue identifies where the problem was found, its severity, the
45/// specific field (if applicable), and a human-readable message.
46#[derive(Debug, Clone)]
47pub struct ValidationIssue {
48    /// Whether this is an error or a warning.
49    pub level: IssueLevel,
50    /// Where the issue was found (file, repo, or main section).
51    pub location: IssueLocation,
52    /// The specific option field name (e.g., `"baseurl"`, `"gpgkey"`), if applicable.
53    pub field: Option<String>,
54    /// A human-readable description of the issue.
55    pub message: String,
56}
57
58/// Severity level for a validation issue.
59///
60/// - [`Error`](IssueLevel::Error) — configuration is invalid and should not be used.
61/// - [`Warning`](IssueLevel::Warning) — advisory notice; configuration may still work.
62#[non_exhaustive]
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum IssueLevel {
65    /// A hard error: the configuration is invalid.
66    Error,
67    /// A soft warning: the configuration may still work but is suspicious.
68    Warning,
69}
70
71/// Identifies where a validation issue was found.
72#[non_exhaustive]
73#[derive(Debug, Clone)]
74pub enum IssueLocation {
75    /// The issue is in a specific `.repo` file on disk.
76    File(String),
77    /// The issue is in a specific repository section.
78    Repo(RepoId),
79    /// The issue is in the `[main]` section.
80    Main,
81}
82
83impl Default for ValidationReport {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl ValidationReport {
90    /// Create a new empty validation report.
91    pub fn new() -> Self {
92        ValidationReport {
93            errors: Vec::new(),
94            warnings: Vec::new(),
95        }
96    }
97
98    /// Returns `true` if there are no errors (warnings are ignored).
99    ///
100    /// A report with only warnings is still considered valid.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use dnf_repofile::ValidationReport;
106    ///
107    /// let report = ValidationReport::new();
108    /// assert!(report.is_ok());
109    /// ```
110    #[must_use]
111    pub fn is_ok(&self) -> bool {
112        self.errors.is_empty()
113    }
114
115    /// Returns `true` if there are any issues (errors or warnings).
116    #[must_use]
117    pub fn has_issues(&self) -> bool {
118        !self.errors.is_empty() || !self.warnings.is_empty()
119    }
120}
121
122impl Repo {
123    /// Validate a single repository's configuration.
124    ///
125    /// Checks:
126    ///
127    /// - At least one URL source is present (`baseurl`, `mirrorlist`, or `metalink`).
128    /// - `baseurl` is not set alongside `mirrorlist` or `metalink` (warns of
129    ///   potential ambiguity).
130    /// - `gpgkey` is set without `gpgcheck` or `repo_gpgcheck` being enabled (warns).
131    #[must_use]
132    pub fn validate(&self) -> ValidationReport {
133        let mut r = ValidationReport::new();
134
135        if self.baseurl.is_empty() && self.mirrorlist.is_none() && self.metalink.is_none() {
136            let issue = ValidationIssue {
137                level: IssueLevel::Error,
138                location: IssueLocation::Repo(self.id.clone()),
139                field: Some("baseurl".into()),
140                message:
141                    "repo must have at least one URL source (baseurl, mirrorlist, or metalink)"
142                        .into(),
143            };
144            r.errors.push(issue);
145        }
146
147        if !self.baseurl.is_empty() && (self.mirrorlist.is_some() || self.metalink.is_some()) {
148            r.warnings.push(ValidationIssue {
149                level: IssueLevel::Warning,
150                location: IssueLocation::Repo(self.id.clone()),
151                field: Some("baseurl".into()),
152                message:
153                    "baseurl and mirrorlist/metalink both set; DNF may ignore mirrorlist/metalink"
154                        .into(),
155            });
156        }
157
158        if !self.gpgkey.is_empty()
159            && self.gpgcheck != Some(DnfBool::True)
160            && self.repo_gpgcheck != Some(DnfBool::True)
161        {
162            r.warnings.push(ValidationIssue {
163                level: IssueLevel::Warning,
164                location: IssueLocation::Repo(self.id.clone()),
165                field: Some("gpgkey".into()),
166                message: "gpgkey is set but gpgcheck and repo_gpgcheck are not enabled".into(),
167            });
168        }
169
170        r
171    }
172}
173
174impl MainConfig {
175    /// Validate the `[main]` configuration.
176    ///
177    /// Currently returns an empty (valid) report. Future releases will add
178    /// validation rules for the `[main]` section.
179    #[must_use]
180    pub fn validate(&self) -> ValidationReport {
181        ValidationReport::new()
182    }
183}