Skip to main content

prns_config/
diagnostic.rs

1use std::fmt;
2
3use crate::configobj::SourceLocations;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ConfigSeverity {
7    Warning,
8    Error,
9}
10
11impl fmt::Display for ConfigSeverity {
12    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13        match self {
14            ConfigSeverity::Warning => formatter.write_str("warning"),
15            ConfigSeverity::Error => formatter.write_str("error"),
16        }
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ConfigDiagnosticCode {
22    Syntax,
23    MisplacedKey,
24    UnknownKey,
25    PersistedRuntimeMetadata,
26    UnknownSection,
27    MissingRequiredKey,
28    InvalidValue,
29    ConflictingAliases,
30    RedundantAliases,
31    UnsupportedInterface,
32    UnsupportedTransport,
33    UnsupportedSetting,
34    IneffectiveSetting,
35    ImplicitOff,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ConfigFix {
40    DisableInterface {
41        name: String,
42    },
43    InsertValue {
44        path: String,
45        accepted: String,
46    },
47    ReplaceValue {
48        path: String,
49        accepted: String,
50    },
51    RemoveValue {
52        path: String,
53        safety: ConfigFixSafety,
54    },
55    ResolveAliases {
56        path: String,
57        aliases: Vec<String>,
58    },
59    ChooseInterfaceType {
60        name: String,
61    },
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ConfigFixSafety {
66    Safe,
67    Guided,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SecretDisplay {
72    Redacted,
73    Revealed,
74}
75
76impl ConfigFix {
77    pub const fn is_safe(&self) -> bool {
78        matches!(
79            self,
80            Self::DisableInterface { .. }
81                | Self::RemoveValue {
82                    safety: ConfigFixSafety::Safe,
83                    ..
84                }
85        )
86    }
87}
88
89impl ConfigDiagnosticCode {
90    pub const fn as_str(self) -> &'static str {
91        match self {
92            ConfigDiagnosticCode::Syntax => "syntax",
93            ConfigDiagnosticCode::MisplacedKey => "misplaced_key",
94            ConfigDiagnosticCode::UnknownKey => "unknown_key",
95            ConfigDiagnosticCode::PersistedRuntimeMetadata => "persisted_runtime_metadata",
96            ConfigDiagnosticCode::UnknownSection => "unknown_section",
97            ConfigDiagnosticCode::MissingRequiredKey => "missing_required_key",
98            ConfigDiagnosticCode::InvalidValue => "invalid_value",
99            ConfigDiagnosticCode::ConflictingAliases => "conflicting_aliases",
100            ConfigDiagnosticCode::RedundantAliases => "redundant_aliases",
101            ConfigDiagnosticCode::UnsupportedInterface => "unsupported_interface",
102            ConfigDiagnosticCode::UnsupportedTransport => "unsupported_transport",
103            ConfigDiagnosticCode::UnsupportedSetting => "unsupported_setting",
104            ConfigDiagnosticCode::IneffectiveSetting => "ineffective_setting",
105            ConfigDiagnosticCode::ImplicitOff => "implicit_off",
106        }
107    }
108
109    pub const fn severity(self) -> ConfigSeverity {
110        match self {
111            ConfigDiagnosticCode::UnknownKey
112            | ConfigDiagnosticCode::PersistedRuntimeMetadata
113            | ConfigDiagnosticCode::UnknownSection
114            | ConfigDiagnosticCode::RedundantAliases
115            | ConfigDiagnosticCode::UnsupportedSetting
116            | ConfigDiagnosticCode::IneffectiveSetting
117            | ConfigDiagnosticCode::ImplicitOff => ConfigSeverity::Warning,
118            ConfigDiagnosticCode::Syntax
119            | ConfigDiagnosticCode::MisplacedKey
120            | ConfigDiagnosticCode::MissingRequiredKey
121            | ConfigDiagnosticCode::InvalidValue
122            | ConfigDiagnosticCode::ConflictingAliases
123            | ConfigDiagnosticCode::UnsupportedInterface
124            | ConfigDiagnosticCode::UnsupportedTransport => ConfigSeverity::Error,
125        }
126    }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct ConfigDiagnostic {
131    code: ConfigDiagnosticCode,
132    source: String,
133    line: usize,
134    path: String,
135    value: Option<String>,
136    message: String,
137    accepted: Option<String>,
138    correction: String,
139    fixes: Vec<ConfigFix>,
140}
141
142impl ConfigDiagnostic {
143    #[allow(clippy::too_many_arguments)]
144    pub(crate) fn new(
145        code: ConfigDiagnosticCode,
146        source: impl Into<String>,
147        line: usize,
148        path: impl Into<String>,
149        value: Option<String>,
150        message: impl Into<String>,
151        accepted: Option<String>,
152        correction: impl Into<String>,
153    ) -> Self {
154        let path = path.into();
155        let fixes = default_fixes(code, &path, accepted.as_deref());
156        Self {
157            code,
158            source: source.into(),
159            line,
160            path,
161            value,
162            message: message.into(),
163            accepted,
164            correction: correction.into(),
165            fixes,
166        }
167    }
168
169    pub const fn severity(&self) -> ConfigSeverity {
170        self.code.severity()
171    }
172
173    pub const fn code(&self) -> ConfigDiagnosticCode {
174        self.code
175    }
176
177    pub fn source(&self) -> &str {
178        &self.source
179    }
180
181    pub const fn line(&self) -> usize {
182        self.line
183    }
184
185    pub fn path(&self) -> &str {
186        &self.path
187    }
188
189    pub fn value(&self) -> Option<&str> {
190        self.value.as_deref()
191    }
192
193    pub fn message(&self) -> &str {
194        &self.message
195    }
196
197    pub fn accepted(&self) -> Option<&str> {
198        self.accepted.as_deref()
199    }
200
201    pub fn correction(&self) -> &str {
202        &self.correction
203    }
204
205    pub fn fixes(&self) -> &[ConfigFix] {
206        &self.fixes
207    }
208
209    pub(crate) fn with_fixes(mut self, fixes: Vec<ConfigFix>) -> Self {
210        self.fixes = fixes;
211        self
212    }
213
214    pub fn display_with(&self, secrets: SecretDisplay) -> DisplayedConfigDiagnostic<'_> {
215        DisplayedConfigDiagnostic {
216            diagnostic: self,
217            secrets,
218        }
219    }
220}
221
222fn default_fixes(code: ConfigDiagnosticCode, path: &str, accepted: Option<&str>) -> Vec<ConfigFix> {
223    let interface = interface_name(path);
224    match code {
225        ConfigDiagnosticCode::MissingRequiredKey if path.ends_with(" > type") => interface
226            .into_iter()
227            .flat_map(|name| {
228                [
229                    ConfigFix::ChooseInterfaceType { name: name.clone() },
230                    ConfigFix::DisableInterface { name },
231                ]
232            })
233            .collect(),
234        ConfigDiagnosticCode::MissingRequiredKey => accepted
235            .map(|accepted| ConfigFix::InsertValue {
236                path: path.to_string(),
237                accepted: accepted.to_string(),
238            })
239            .into_iter()
240            .chain(interface.map(|name| ConfigFix::DisableInterface { name }))
241            .collect(),
242        ConfigDiagnosticCode::InvalidValue => accepted
243            .map(|accepted| ConfigFix::ReplaceValue {
244                path: path.to_string(),
245                accepted: accepted.to_string(),
246            })
247            .into_iter()
248            .chain(interface.map(|name| ConfigFix::DisableInterface { name }))
249            .collect(),
250        ConfigDiagnosticCode::ConflictingAliases => vec![ConfigFix::ResolveAliases {
251            path: path.to_string(),
252            aliases: Vec::new(),
253        }],
254        ConfigDiagnosticCode::RedundantAliases => {
255            vec![ConfigFix::RemoveValue {
256                path: path.to_string(),
257                safety: ConfigFixSafety::Safe,
258            }]
259        }
260        ConfigDiagnosticCode::UnknownKey => vec![ConfigFix::RemoveValue {
261            path: path.to_string(),
262            safety: ConfigFixSafety::Guided,
263        }],
264        ConfigDiagnosticCode::PersistedRuntimeMetadata => vec![ConfigFix::RemoveValue {
265            path: path.to_string(),
266            safety: ConfigFixSafety::Safe,
267        }],
268        ConfigDiagnosticCode::UnsupportedInterface => interface
269            .into_iter()
270            .flat_map(|name| {
271                [
272                    ConfigFix::ChooseInterfaceType { name: name.clone() },
273                    ConfigFix::DisableInterface { name },
274                ]
275            })
276            .collect(),
277        ConfigDiagnosticCode::UnsupportedTransport => interface
278            .map(|name| vec![ConfigFix::DisableInterface { name }])
279            .unwrap_or_default(),
280        ConfigDiagnosticCode::ImplicitOff => vec![ConfigFix::ReplaceValue {
281            path: path.to_string(),
282            accepted: "off".to_string(),
283        }],
284        ConfigDiagnosticCode::Syntax
285        | ConfigDiagnosticCode::MisplacedKey
286        | ConfigDiagnosticCode::UnknownSection
287        | ConfigDiagnosticCode::UnsupportedSetting
288        | ConfigDiagnosticCode::IneffectiveSetting => Vec::new(),
289    }
290}
291
292fn interface_name(path: &str) -> Option<String> {
293    let start = path.find("[[")? + 2;
294    let rest = &path[start..];
295    let end = rest.find("]]")?;
296    let name = rest[..end].trim();
297    (!name.is_empty()).then(|| name.to_string())
298}
299
300impl fmt::Display for ConfigDiagnostic {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        self.display_with(SecretDisplay::Redacted).fmt(formatter)
303    }
304}
305
306pub struct DisplayedConfigDiagnostic<'a> {
307    diagnostic: &'a ConfigDiagnostic,
308    secrets: SecretDisplay,
309}
310
311impl fmt::Display for DisplayedConfigDiagnostic<'_> {
312    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313        let diagnostic = self.diagnostic;
314        write!(
315            formatter,
316            "{}:{}: {}[{}] {}: {}",
317            diagnostic.source,
318            diagnostic.line,
319            diagnostic.severity(),
320            diagnostic.code.as_str(),
321            diagnostic.path,
322            diagnostic.message,
323        )?;
324        if let Some(value) = &diagnostic.value {
325            if self.secrets == SecretDisplay::Revealed || !secret_path(&diagnostic.path) {
326                write!(formatter, "; found {value:?}")?;
327            } else {
328                formatter.write_str("; found <redacted>")?;
329            }
330        }
331        if let Some(accepted) = &diagnostic.accepted {
332            write!(formatter, "; accepted: {accepted}")?;
333        }
334        if self.secrets == SecretDisplay::Redacted && secret_path(&diagnostic.path) {
335            formatter.write_str("; fix: correct the secret-bearing setting")
336        } else {
337            write!(formatter, "; fix: {}", diagnostic.correction)
338        }
339    }
340}
341
342fn secret_path(path: &str) -> bool {
343    matches!(
344        path.rsplit(" > ").next().map(str::trim),
345        Some("pass_phrase" | "passphrase" | "rpc_key")
346    )
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct ConfigErrors {
351    diagnostics: Vec<ConfigDiagnostic>,
352}
353
354impl ConfigErrors {
355    pub(crate) fn new(diagnostics: Vec<ConfigDiagnostic>) -> Self {
356        Self { diagnostics }
357    }
358
359    pub fn diagnostics(&self) -> &[ConfigDiagnostic] {
360        &self.diagnostics
361    }
362
363    pub fn len(&self) -> usize {
364        self.diagnostics.len()
365    }
366
367    pub fn is_empty(&self) -> bool {
368        self.diagnostics.is_empty()
369    }
370}
371
372impl fmt::Display for ConfigErrors {
373    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374        for (index, diagnostic) in self.diagnostics.iter().enumerate() {
375            if index != 0 {
376                formatter.write_str("\n")?;
377            }
378            write!(formatter, "{diagnostic}")?;
379        }
380        Ok(())
381    }
382}
383
384impl std::error::Error for ConfigErrors {}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct ConfigReport<T> {
388    pub value: T,
389    pub warnings: Vec<ConfigDiagnostic>,
390    pub source: String,
391    pub locations: SourceLocations,
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn diagnostics_redact_secret_values_unless_revealed() {
400        let diagnostic = ConfigDiagnostic::new(
401            ConfigDiagnosticCode::InvalidValue,
402            "config",
403            4,
404            "[interfaces] > [[WiFi]] > pass_phrase",
405            Some("private value".to_string()),
406            "invalid passphrase",
407            Some("a valid passphrase".to_string()),
408            "replace the value",
409        );
410
411        assert!(!diagnostic.to_string().contains("private value"));
412        assert!(diagnostic
413            .display_with(SecretDisplay::Revealed)
414            .to_string()
415            .contains("private value"));
416    }
417}